Java
Z
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
JavaのGenericsを使いこなす — 基礎から境界型パラメータまで
Java Genericsの基本から、extends/superによる境界型パラメータ、ワイルドカード、型消去まで実例を交えて解説します。
一言結論
Genericsの境界型パラメータ「? extends T」は読み取り専用、「? super T」は書き込み専用というPECS原則を覚えれば、柔軟なAPIを型安全に設計できる。
なぜ Generics が必要か
Generics がない場合、コレクションは Object 型として扱われ、取り出すたびにキャストが必要でした。
// Generics なし(危険)
List list = new ArrayList();
list.add("hello");
list.add(123); // 混在してしまう
String s = (String) list.get(1); // ClassCastException!
// Generics あり(安全)
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // コンパイルエラーで防いでくれる
String s = list.get(0); // キャスト不要
ジェネリクスクラスの定義
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 使い方
Box<String> strBox = new Box<>("hello");
Box<Integer> intBox = new Box<>(42);
String s = strBox.get(); // キャスト不要
ジェネリクスメソッド
クラス全体ではなく、メソッド単位で型パラメータを定義できます。
public class Utils {
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) return null;
return list.get(0);
}
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
}
// 呼び出し(型推論で <String> は省略可能)
String first = Utils.getFirst(List.of("A", "B", "C"));
int bigger = Utils.max(10, 20);
境界型パラメータ
上限境界 <T extends SomeClass>
T が SomeClass のサブタイプであることを保証します。
public <T extends Number> double sum(List<T> list) {
return list.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Integer も Double も Number のサブクラス
sum(List.of(1, 2, 3)); // OK
sum(List.of(1.1, 2.2)); // OK
// sum(List.of("a", "b")); // コンパイルエラー
ワイルドカード ?
型が何でもよい場合に使います。
// ? extends T: Tまたはそのサブタイプを読み取る(プロデューサー)
public double sumAll(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
// ? super T: Tまたはその親タイプに書き込む(コンシューマー)
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
PECS 原則(Producer Extends, Consumer Super):
- リストから読む(プロデューサー)→
? extends T - リストに書く(コンシューマー)→
? super T
複数の境界
// Serializable かつ Comparable を実装した型
public <T extends Comparable<T> & Serializable> T findMax(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
型消去(Type Erasure)
Java のジェネリクスはコンパイル時の機能です。実行時には型情報が消えます(型消去)。
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// 実行時には同じ型
System.out.println(strings.getClass() == integers.getClass()); // true
// これはコンパイルエラー(実行時に区別できないため)
// if (obj instanceof List<String>) { } // NG
if (obj instanceof List<?>) { } // OK
よくある間違い
// NG: ジェネリクス型の配列は作れない
List<String>[] array = new ArrayList<String>[10]; // コンパイルエラー
// OK: ワイルドカードを使う
List<?>[] array = new ArrayList<?>[10];
// NG: プリミティブ型は使えない
List<int> list; // コンパイルエラー
// OK: ラッパークラスを使う
List<Integer> list = new ArrayList<>();
まとめ
| 記法 | 意味 |
|---|---|
<T> | 任意の型 T |
<T extends Foo> | Foo のサブタイプ |
<T extends Foo & Bar> | Foo かつ Bar を実装 |
<?> | 不明な型(読み取り専用) |
<? extends Foo> | Foo のサブタイプ(読み取り) |
<? super Foo> | Foo のスーパータイプ(書き込み) |
Generics はコードの再利用性と型安全性を同時に高める強力な機能です。最初は難しく感じますが、コレクションを使う時点で毎日使っています。