擦除法#
コンパイラは型<T>
を Object として扱う#
public class Pair<T> {
private final T first;
private final T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
Java のジェネリクスはコンパイル時に行われ、コンパイラは上記のコードを擦除法で処理し、JVM が実際に見るコードは以下の通りです:
public class Pair {
private final Object first;
private final Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
コンパイラは<T>
に基づいて安全なキャストを実現する#
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
コンパイラは T の型に基づいて安全にキャストします:
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
擦除法の限界#
<T>
は基本型にはできません。なぜなら、擦除法はそれをObject
に置き換えるからです。- ジェネリクスを持つ
Class
を取得することはできません。なぜなら、ジェネリクスはコンパイル時に処理されるため、取得されるのはすべてPair<Object>
です。
例えば:
@Test
void testGetClass() {
Pair<String> p1 = new Pair<>("Hello", "World");
Pair<Integer> p2 = new Pair<>(114, 514);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
assertEquals(c1, c2);
assertEquals(c1, Pair.class);
}
- 不適切なオーバーライドメソッド
class Pair<T> {
public boolean equals(T t) {}
}
これは、定義されたequals(T t)
メソッドが実際にはequals(Object t)
に擦り替えられるためです。このメソッドはObject
から継承されたものであり、コンパイラは実際にオーバーライドされることになるジェネリックメソッドの定義を阻止します。
ジェネリクスの継承#
クラスはジェネリッククラスを継承できます。ジェネリック型を継承した場合、サブクラスは親クラスのジェネリック型を取得できます。例えば:IntPair
は親クラスのジェネリック型Integer
を取得できます。
public class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
class IntPairTest {
@Test
void getSuperParameterizedType() {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 多くの型があるかもしれません
Type firstType = types[0];
Class<?> typeClass = (Class<?>) firstType;
assertEquals(typeClass, Integer.class);
}
}
}
extends
ワイルドカード#
public class PairHelper {
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
上記のように、add
関数が受け取る引数の型はPair<Number>
であるため、Pair<Number>
を渡すことには問題ありません。しかし、Pair<Integer>
を渡すとコンパイルエラーが発生します。これは、Integer
がNumber
のサブクラスであるにもかかわらず、Pair<Integer>
はPair<Number>
のサブクラスではないためです。
この問題を解決するために、Pair<? extends Number>
を使用することで、ジェネリック型がNumber
のサブクラスであるPair
型を受け取ることができます:
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
@Test
void addNumberBased() {
assertEquals(579, add(new Pair<Integer>(123, 456)));
}
読み取り#
List<? extends Number>
からNumber
型を取得できます。なぜなら、その要素はNumber
型またはNumber
のサブタイプだからです。List<? extends Number>
からInteger
型を取得することはできません。なぜなら、その保存されている要素がDouble
である可能性があるからです。List<? extends Number>
からDouble
型を取得することはできません。なぜなら、その保存されている要素がInteger
である可能性があるからです。
書き込み#
List<? extends Number>
にNumber
を追加することはできません。なぜなら、その保存されている要素がInteger
またはDouble
である可能性があるからです。List<? extends Number>
にInteger
を追加することはできません。なぜなら、その保存されている要素がDouble
である可能性があるからです。List<? extends Number>
にNumber
を追加することはできません。なぜなら、その保存されている要素がInteger
である可能性があるからです。
public class ExtendsTest {
List<? extends Number> nums1 = new ArrayList<Number>();
List<? extends Number> nums2 = new ArrayList<Integer>();
List<? extends Number> nums3 = new ArrayList<Double>();
void get() {
Number x = nums1.get(0);
// Integer y = nums2.get(0); // コンパイルエラー
// Double z = nums3.get(0); // コンパイルエラー
}
void set() {
/*
nums1.add(new Number() {
@Override
public int intValue() {
return 0;
}
@Override
public long longValue() {
return 0;
}
@Override
public float floatValue() {
return 0;
}
@Override
public double doubleValue() {
return 0;
}
});
*/ // コンパイルエラー
// nums2.add(new Integer(1)); // コンパイルエラー
// nums3.add(new Double(1.2)); // コンパイルエラー
}
}
super
ワイルドカード#
<? super T>
はワイルドカードの下限を示し、具体的なジェネリックパラメータは次の条件を満たす必要があります:ジェネリックパラメータはT
型またはその親クラスでなければなりません。
読み取り#
List<? super Integer>
型のオブジェクトからInteger
型のデータを読み取ることは保証されません。なぜなら、それがList<Number>
型である可能性があるからです。List<? super Integer>
型のオブジェクトからNumber
型のデータを読み取ることは保証されません。なぜなら、それがList<Object>
型である可能性があるからです。- 唯一保証されるのは、
List<? super Integer>
型のオブジェクトからObject
オブジェクトのインスタンスを取得できることです。
書き込み#
上記の例のList<? super Integer>
配列オブジェクトに対して:
Integer
オブジェクトをarray
に追加できます。また、Integer
のサブクラスのオブジェクトもarray
に追加できます。なぜなら、Integer
およびその親クラスはInteger
のサブクラスのオブジェクトを受け入れることができるからです。Double/Number/Object
など、Integer
のサブクラスでないオブジェクトをarray
に追加することはできません。
extends
とsuper
#
List<? super Integer> l1
の場合:#
- 正しい理解:
? super Integer
はジェネリックパラメータを制限します。l1
のジェネリックパラメータをT
とすると、T
はInteger
またはInteger
の親クラスであるため、Integer
またはInteger
のサブクラスのオブジェクトをl1
に追加できます。 - 誤った理解:
? super Integer
は挿入される要素の型を制限します。したがって、Integer
またはInteger
の親クラスのオブジェクトはすべてl1
に挿入できます。
List<? extends Integer> l2
の場合:#
- 正しい理解:
? extends Integer
はジェネリックパラメータを制限します。l2
のジェネリックパラメータをT
とすると、T
はInteger
またはInteger
のサブクラスであるため、X
というクラスを見つけることはできません。X
はジェネリックパラメータT
のサブクラスであるため、要素をl2
に追加することはできません。ただし、ジェネリックパラメータT
がInteger
またはInteger
のサブクラスであることがわかっているため、l2
から要素を読み取ることができ(取得した要素の型はInteger
またはInteger
のサブクラス)、Integer
に格納できます。 - 誤った理解:
? extends Integer
は挿入される要素の型を制限します。したがって、Integer
またはInteger
のサブクラスのオブジェクトはすべてl2
に挿入できます。
応用:PECS#
PECS 原則:Producer Extends, Consumer Super
- Producer extends:
List
からT
型のデータを提供する必要がある場合(つまり、List
からT
型のデータを読み取ることを希望する場合)、? extends T
を使用する必要があります。例えば、List<? extends Integer>
。ただし、このList
にデータを追加することはできません。 - Consumer Super:
List
にT
型のデータを消費する必要がある場合(つまり、T
型のデータをList
に書き込むことを希望する場合)、? super T
を使用する必要があります。例えば、List<? super Integer>
。ただし、このList
から読み取るデータの型は保証されません。 - 読み取りも書き込みも希望する場合は、ジェネリックパラメータの型を明示的に宣言する必要があります。例えば、
List<Integer>
。
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
}