なぜコードが正常にコンパイルされるのか不思議に思われるコードに遭遇しました。
public class Main {
public static void main(String[] args) {
String s = newList(); // why does this line compile?
System.out.println(s);
}
private static <T extends List<Integer>> T newList() {
return (T) new ArrayList<Integer>();
}
}
興味深いのは、メソッドnewList
のシグネチャを<T extends ArrayList<Integer>>
で変更すると、機能しなくなることです。
コメントと応答の後に更新:ジェネリック型をメソッドからクラスに移動すると、コードはコンパイルできなくなります。
public class SomeClass<T extends List<Integer>> {
public void main(String[] args) {
String s = newList(); // this doesn't compile anymore
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
メソッドで型パラメーターを宣言する場合、実際の型が制約を満たす限り、呼び出し元は実際の型を選択できます。その型は、実際の具象型である必要はありません。抽象的な型、型変数、または交差型の場合もあれば、口語的な言葉である仮説型の場合もあります。したがって、 Mureinikが述べたように は、String
を拡張し、List
を実装する型が存在する可能性があります。呼び出しの交差タイプを手動で指定することはできませんが、タイプ変数を使用してロジックを示すことができます。
_public class Main {
public static <X extends String&List<Integer>> void main(String[] args) {
String s = Main.<X>newList();
System.out.println(s);
}
private static <T extends List<Integer>> T newList() {
return (T) new ArrayList<Integer>();
}
}
_
もちろん、newList()
はそのような型を返すという期待に応えることはできませんが、それはこのメソッドの定義(または実装)の問題です。 ArrayList
をT
にキャストすると、「未チェック」の警告が表示されます。唯一可能な正しい実装は、ここでnull
を返すことです。これにより、メソッドはまったく役に立たなくなります。
最初のステートメントを繰り返すポイントは、ジェネリックメソッドのcallerが型パラメーターの実際の型を選択することです。対照的に、ジェネリックclassを次のように宣言すると、
_public class SomeClass<T extends List<Integer>> {
public void main(String[] args) {
String s = newList(); // this doesn't compile anymore
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
_
タイプパラメータはクラスのコントラクトの一部であるため、インスタンスを作成する人はだれでも、そのインスタンスの実際のタイプを選択します。インスタンスメソッドmain
はそのクラスの一部であり、その規約に従う必要があります。必要なT
を選択することはできません。 T
の実際のタイプが設定されており、Javaでは通常、T
が何であるかを知ることさえできません。
ジェネリックプログラミングの要点は、型パラメーターに選択されている実際の型とは無関係に機能するコードを記述することです。
ただし、anotherの独立したインスタンスを好きなタイプで作成し、メソッドを呼び出すことができます。
_public class SomeClass<T extends List<Integer>> {
public <X extends String&List<Integer>> void main(String[] args) {
String s = new SomeClass<X>().newList();
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
_
ここでは、新しいインスタンスの作成者がそのインスタンスの実際のタイプを選択します。前述のように、その実際の型は具体的な型である必要はありません。
これは、List
がインターフェイスであるためだと思います。 String
がfinal
であるという事実を1秒間無視すると、理論的には、extends String
(s
に割り当てることができる)がimplements List<Integer>
のクラスを持つことができます。 (つまり、newList()
から返される可能性があります)。戻り値の型をインターフェイス(T extends List
)から具象クラス(T extends ArrayList
)に変更すると、コンパイラーはそれらが相互に割り当て可能ではないと推定し、エラーを生成します。
もちろん、これはString
が実際にはfinal
であるために機能しなくなり、コンパイラがこれを考慮に入れることが期待できます。私見、それはバグですが、私はコンパイラの専門家ではないことを認めなければなりませんが、この時点でfinal
修飾子を無視するのには十分な理由があるかもしれません。
なぜこれがコンパイルされるのかわかりません。一方、コンパイル時のチェックを最大限に活用する方法を説明できます。
したがって、newList()
はジェネリックメソッドであり、型パラメーターが1つあります。このパラメーターを指定すると、コンパイラーはそれをチェックします。
コンパイルに失敗しました:
_String s = Main.<String>newList(); // this doesn't compile anymore
System.out.println(s);
_
コンパイルステップをパスします:
_List<Integer> l = Main.<ArrayList<Integer>>newList(); // this compiles and works well
System.out.println(l);
_
typeパラメータの指定
型パラメーターは、コンパイル時のチェックのみを提供します。これは設計によるものであり、Javaは type erasure を使用し、ジェネリック型に使用します。コンパイラーを機能させるには、コードでこれらの型を指定する必要があります。
インスタンス作成時のタイプパラメータ
最も一般的なケースは、オブジェクトインスタンスのパターンを指定することです。つまりリストの場合:
_List<String> list = new ArrayList<>();
_
ここでは、_List<String>
_がリストアイテムのタイプを指定していることがわかります。一方、new ArrayList<>()
にはありません。代わりに ダイヤモンド演算子 を使用します。つまりJavaコンパイラinfers宣言に基づいた型。
メソッド呼び出しでの暗黙の型パラメーター
静的メソッドを呼び出すときは、別の方法で型を指定する必要があります。時々それをパラメーターとして指定することができます:
_public static <T extends Number> T max(T n1, T n2) {
if (n1.doubleValue() < n2.doubleValue()) {
return n2;
}
return n1;
}
_
あなたはこのようにそれを使うことができます:
_int max = max(3, 4); // implicit param type: Integer
_
またはこのように:
_double max2 = max(3.0, 4.0); // implicit param type: Double
_
メソッド呼び出し時の明示的な型パラメーター:
たとえば、次のようにタイプセーフな空のリストを作成できます。
_List<Integer> noIntegers = Collections.<Integer>emptyList();
_
型パラメーター_<Integer>
_がメソッドemptyList()
に渡されます。唯一の制約は、クラスも指定する必要があることです。つまりあなたはこれを行うことができません:
_import static Java.util.Collections.emptyList;
...
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile
_
ランタイムタイプトークン
これらのトリックがどれも役に立たない場合は、 ランタイムタイプトークン を指定できます。つまりクラスをパラメーターとして提供します。一般的な例は EnumMap です。
_private static enum Letters {A, B, C}; // dummy enum
...
public static void main(String[] args) {
Map<Letters, Integer> map = new EnumMap<>(Letters.class);
}
_