1. ジェネリクスの不変性: List<Number> ≠ List<Integer>
Java のジェネリクスは厳格です。すなわち、不変(invariant)です。これは、ある型が別の型のサブタイプであっても(たとえば Integer は Number のサブタイプ)、それらの型引数を持つコレクション同士は互換ではないことを意味します。
たとえば、あなたがりんごの箱(List<Integer>)を持っているのに、誰かがそれを「果物の箱」(List<Number>)だと言ったとします。一見もっともらしい――りんごは果物です。しかしそうなると、その箱にバナナ(Double)を入れられてしまい、実は「りんご専用」の箱なのにおかしなことになります。
コード例:
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // コンパイルエラー!
コンパイラがここで厳しいのは意図的です。「りんごのリスト」を「果物のリスト」に変えることを許しません。そうしないと、何でも追加できてしまい、実行時にプログラムが壊れる可能性があるからです。
したがって、List<Number> と List<Integer> は、Integer 自体は Number のサブタイプであるにもかかわらず、まったく別の型なのです。
配列はこれと異なります:
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // 許可される(配列は共変)
numArr[0] = 3.14; // 実行時エラー (ArrayStoreException)
結論: ジェネリクスはコンパイル時の型安全性を確保するために不変です。
2. 型パラメータの境界
ときには、ジェネリック・クラスやメソッドが扱える型を制限したくなります。そのために 境界(bounds) を使います。
上限境界(extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
これで Stats は Number のサブタイプ(Integer、Double、Float など)だけを扱えます。Stats<String> を作ろうとするとコンパイルエラーになります。
インターフェースによる制約
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
これで Sorter は Comparable を実装している型だけを扱えます。
複数境界
& を使って複数の制約を同時に指定できます:
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T は Number のサブクラス かつ Comparable<T> を実装していなければなりません。
順序が重要: まずクラス、次にインターフェースを並べます。
3. ワイルドカード入門: ?
ワイルドカード は「ここには何らかの型が来るが、具体的にどの型かは分からない」ということを表す「置き換え」用の型です。
例:
- List<?> — 何でものリスト。
- List<? extends Number> — Number のサブタイプ(たとえば Integer、Double)のリスト。
- List<? super Integer> — Integer のスーパータイプ(たとえば Integer、Number、Object)のリスト。
なぜワイルドカードが必要か?
関連する異なる型のコレクションを扱えるメソッドを書けるようにするためです。
例: 読み取り専用(producer)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // エラー! 要素は追加できません
}
- 要素を Number として読み取れます。
- 追加はできません(null を除く)。
例: 書き込み専用(consumer)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // エラー! どの型が返るかわからない
}
- Integer(またはそのサブタイプ)の要素を追加できます。
- 安全に Integer として読み取ることはできません(Object としてなら可)。
PECS 規則
PECS — 「Producer Extends, Consumer Super」:
- Producer — Extends: コレクションがデータを供給するだけ(producer)の場合は ? extends T を使う。
- Consumer — Super: コレクションがデータを受け取るだけ(consumer)の場合は ? super T を使う。
覚え方: extends は読み取り用(producer)、super は書き込み用(consumer)。
比較: 配列は共変、ジェネリクスは不変
- 配列は共変: Integer[] は Number[] に代入できます。
- ジェネリクスは不変: List<Integer> は List<Number> に代入できません。
ワイルドカードは、ジェネリクスの不変性を部分的に「緩和」できます。
5. ジェネリック・メソッドと型推論; 制約(型消去)
ジェネリック・メソッド
任意の型で動作するジェネリック・メソッドを宣言できます:
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
型推論(type inference)
Java は型引数を「推測」できます:
List<String> strings = List.of("a", "b");
printList(strings); // T = String
ジェネリクスの制約: 型消去
- Java のジェネリクスは型消去で実装されています。コンパイル後、型パラメータの情報は失われます。
- バイトコード上では List<String> と List<Integer> に違いはありません。
- パラメータ化型の配列は作れません: new List<String>[10] — エラー。
- 型引数付きで instanceof は使えません: obj instanceof List<String> — エラー。
6. コレクションと Stream API での実践
リスト間の要素コピー
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src — producer(? extends T)
- dest — consumer(? super T)
使用例:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number は Integer のスーパータイプ
Stream API とワイルドカード
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
ワイルドカードでのフィルタリング
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. よくある間違い
エラー1: Raw type(生の型)。
「生の」型を使うと型チェックが無効になり、実行時エラーにつながります。
List list = new ArrayList(); // raw type — 悪い!
list.add("文字列");
list.add(123); // 何でも追加できてしまう
String s = (String) list.get(1); // ClassCastException!
Raw type は決して使わず、常に型パラメータを指定しましょう: List<String>、List<Integer>。
エラー2: extends での安全でない操作。
? extends ... を持つコレクションに要素を追加しようとすると、コンパイルエラーになります。
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // コンパイルエラー!
エラー3: オーバーロード時の「型消去」による衝突。
ジェネリックの型引数だけが異なるメソッドはオーバーロードできません――型消去後はシグネチャが同一になります。
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // コンパイルエラー!
エラー4: パラメータ化型の配列。
型消去のため、パラメータ化型の配列は作成できません。
List<String>[] arr = new List<String>[10]; // コンパイルエラー!
GO TO FULL VERSION