私は次のクラスを持っています(簡略化されていますが、実際に機能する例です)。
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)
キャストが機能しませんか?
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
を取得します。
このリソースの完全性のために、キャストからジェネリックへのコンパイル済みバイトコードの違いを次に示します。
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
は本当に汎用的ではありませんが、キャストwillがNumber
スーパークラスに対してチェックされるため、このクラス定義を使用すると、実行時に型が強制されます。これを証明するためのバイトコードは次のとおりです。
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
を生成します。
代替ソリューションとして、 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)