web-dev-qa-db-ja.com

ジェネリック型キャスト

私は次のクラスを持っています(簡略化されていますが、実際に機能する例です)。

class Test<T> {
    List<T> l = new ArrayList<>();

    public Test() {
    }

    public void add(Object o) {
        l.add((T)o);
    }
}

そしてテストコード:

Test<Double> t = new Test<>();
t.add(1);
t.add(1.2);
t.add(-5.6e-2);
t.add("hello");

すべてが正常に機能しており、それは私が期待していたことではありません。 addメソッドはClassCastExceptionをスローすべきではありませんか? getメソッドを追加すると、ほぼ同じです:

    public T get(int i) {
        return l.get(i);
    }
.../...
t.get(1);             // OK.
t.get(3);             // OK (?)
Double d = t.get(3);  // throws ClassCastException

なぜ例外がスローされるのは変数の割り当てに限られるのですか? (T)キャストが機能しませんか?

19
gregseth

AddメソッドはClassCastExceptionをスローすべきではありませんか?

いいえ、それはすべきではありません(私はそうしたいのですが)。簡単に言えば、Javaジェネリックの実装では、コードのコンパイル後に型情報が破棄されるため、List<T>Objectを取得でき、addメソッド内のキャストはチェックされません。

なぜ例外がスローされるのは変数の割り当てに限られるのですか?

Doubleへのキャストがあるため、コンパイラーによって挿入されます。 Javaコンパイラは、getの戻り値の型がT、つまりDoubleであることを認識しているため、結果が割り当てられている変数dの型と一致するキャストを挿入します。

ジェネリックセーフキャストを実装する方法は次のとおりです。

class Test<T> {
    private final Class<T> cl;
    List<T> l = new ArrayList<>();

    public Test(Class<T> c) {
        cl = c;
    }

    public void add(Object o) {
        l.add(cl.cast(o));
    }
}

これでキャストはClass<T>オブジェクトによって実行されるため、不正なタイプのオブジェクトを挿入しようとすると、ClassCastExceptionを取得します。

21
dasblinkenlight

このリソースの完全性のために、キャストからジェネリックへのコンパイル済みバイトコードの違いを次に示します。

public void add(Java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: invokeinterface #7,  2            // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
    10: pop
    11: return

そしてジェネリックなしのDoubleへの明示的なキャスト:

public void add(Java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class Java/lang/Double
     8: invokeinterface #8,  2            // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

ジェネリックスを備えたバージョンはcheckcast命令をまったく実行しないことがわかります( type erasure のおかげで)、一致しないデータをデータに与えるときに例外を期待するべきではありませんこれはより厳密に強制されないのは残念ですが、ジェネリックスはより厳密なcompile-time型チェックを行うためのものであり、型の消去により、実行時に多くの助けとなる。

Javaは、関数の引数の型をチェックして、型が一致するかどうか、または型の昇格を実行できるかどうかを確認します。あなたの場合、Stringは引数の型であり、これはObjectに昇格できます。これは、関数呼び出しが機能することを確認するコンパイル時の型チェックの範囲です。

いくつかのオプションがあり、dasblinkenlightのソリューションはおそらく最もエレガントです。 (たとえば、継承されたaddメソッドをオーバーライドしている場合や、addメソッドを渡す予定がある場合などは、メソッドのシグネチャを変更できない場合があります)。

役立つ別のオプションは、無制限のパラメータの代わりにboundedタイプパラメータを使用することです。バインドされていない型のパラメーターは、型の消去のためにコンパイル後に完全に失われますが、バインドされた型のパラメーターを使用すると、ジェネリック型のインスタンスが拡張する必要のある型に置き換えられます。

class Test<T extends Number> {

もちろん、現時点ではTは本当に汎用的ではありませんが、キャストwillNumberスーパークラスに対してチェックされるため、このクラス定義を使用すると、実行時に型が強制されます。これを証明するためのバイトコードは次のとおりです。

public void add(Java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class Java/lang/Number
     8: invokeinterface #8,  2            // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

このクラス定義は、文字列を追加しようとするときに、必要なClassCastExceptionを生成します。

7
Purag

代替ソリューションとして、 Collections.checkedList

class Test<T> {
    List<T> l;

    public Test(Class<T> c) {
        l = Collections.checkedList(new ArrayList<T>(), c);
    }

    public void add(Object o) {
        l.add((T) o);
    }
}

これにより、次の例外が発生します。

Exception in thread "main" Java.lang.ClassCastException: Attempt to insert 
  class Java.lang.Integer element into collection with element type class Java.lang.Double
    at Java.util.Collections$CheckedCollection.typeCheck(Collections.Java:3037)
    at Java.util.Collections$CheckedCollection.add(Collections.Java:3080)
    at Test.add(Test.Java:13)
7
Tagir Valeev