タイトル: ジェネリクス
SEOタイトル: Java ジェネリクスの基本と実践(型パラメータ・ワイルドカード・PECS・型消去)
| この記事の要点 |
|
ジェネリクスとは何か
Java のジェネリクスは、クラスやメソッドの宣言時に「型パラメータ」を使い、利用時に具体的な型を指定する仕組みです。Java 5 で導入され、コレクション API(List, Map, Set)の型安全を実現する核となっています。
// ジェネリクス導入前 (Java 1.4 以前)
List list = new ArrayList();
list.add("hello");
list.add(123); // 何でも入れられる
String s = (String) list.get(1); // 実行時に ClassCastException
// ジェネリクス導入後 (Java 5+)
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123); // コンパイルエラー!
String s = list.get(0); // キャスト不要
ジェネリッククラスの定義
// 単一型パラメータ
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// 利用
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get(); // String 型として取り出せる
Box<Integer> intBox = new Box<>();
intBox.set(42);
int n = intBox.get(); // Integer → int (アンボクシング)
// 複数型パラメータ
public class Pair<K, V> {
private K key;
private V value;
public Pair(K k, V v) { this.key = k; this.value = v; }
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> p = new Pair<>("age", 30);
ジェネリックメソッド
クラス全体ではなくメソッド単位で型パラメータを宣言できます。戻り値型の左側に <T> を書くのが特徴:
public class Util {
// ジェネリックメソッド
public static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// 複数型パラメータ
public static <K, V> Map<V, K> reverse(Map<K, V> src) {
Map<V, K> result = new HashMap<>();
for (Map.Entry<K, V> e : src.entrySet()) {
result.put(e.getValue(), e.getKey());
}
return result;
}
}
// 型推論で T が解決される (Java 7+ のダイヤモンド演算子も同様)
String s = Util.firstOrNull(List.of("a", "b", "c"));
Integer n = Util.firstOrNull(List.of(1, 2, 3));
// 明示指定も可能
String s2 = Util.<String>firstOrNull(List.of("a", "b"));
境界付き型パラメータ (Bounded Type Parameter)
<T extends Number> で「T は Number またはサブクラス」と制限できます。
// T は Number 以下の型のみ許可
public static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) {
total += n.doubleValue(); // Number のメソッドが呼べる
}
return total;
}
sum(List.of(1, 2, 3)); // OK (Integer)
sum(List.of(1.5, 2.5)); // OK (Double)
// sum(List.of("a", "b")); // コンパイルエラー
// 複数の境界 (& で AND)
public static <T extends Number & Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T x : list) if (x.compareTo(best) > 0) best = x;
return best;
}
ワイルドカードと PECS 原則
ワイルドカード ? は「何らかの型」を表します。PECS (Producer Extends, Consumer Super) という原則で使い分けます:
| 形式 | 意味 | 用途 | 例 |
|---|---|---|---|
List<T> | 厳密に T | 読み書き両方 | List<Integer> |
List<? extends T> | T またはサブクラス | Producer (読み取り専用) | 合計を計算する |
List<? super T> | T またはスーパークラス | Consumer (書き込み) | 要素を追加する |
List<?> | 未知の型 | 型を問わない走査 | サイズ計算など |
// Producer Extends: 読み取り専用 (要素を取り出す)
public static double sumOfList(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue(); // 取り出しは OK
// list.add(1); // ← コンパイルエラー (何の型が来るかわからない)
return total;
}
sumOfList(List.of(1, 2, 3)); // List<Integer> OK
sumOfList(List.of(1.5, 2.5)); // List<Double> OK
// Consumer Super: 書き込み専用 (要素を入れる)
public static void addNumbers(List<? super Integer> list) {
for (int i = 0; i < 10; i++) list.add(i); // 追加は OK
// Integer x = list.get(0); // ← Object としてしか取り出せない
}
List<Number> nums = new ArrayList<>();
addNumbers(nums); // OK: Number は Integer のスーパー
型消去 (Type Erasure)
Java のジェネリクスはコンパイル時のみ存在し、実行時には消去されます。互換性のための設計ですが、いくつかの制約が生まれます:
// コンパイル時
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
// 実行時 (型消去後)
List a = new ArrayList(); // 同じ生型
List b = new ArrayList();
System.out.println(a.getClass() == b.getClass()); // true!
// 制約1: new T() できない
class Bad<T> {
T create() { return new T(); } // ← コンパイルエラー
}
// 制約2: instanceof で型パラメータを使えない
<T> boolean check(Object o) {
return o instanceof T; // ← コンパイルエラー
}
// 制約3: ジェネリック配列を作れない
T[] arr = new T[10]; // ← コンパイルエラー
// 制約4: static フィールドで型パラメータ不可
class Bad2<T> {
static T cache; // ← コンパイルエラー
}
Type Token (Class) で型消去を回避
// 実行時に型を知りたい場合は Class オブジェクトを渡す
public class TypeSafeMap {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T value) {
map.put(type, value);
}
public <T> T get(Class<T> type) {
return type.cast(map.get(type)); // 安全なキャスト
}
}
TypeSafeMap m = new TypeSafeMap();
m.put(String.class, "hello");
m.put(Integer.class, 42);
String s = m.get(String.class);
Integer n = m.get(Integer.class);
ダイヤモンド演算子 (Java 7+)
// Java 5/6
Map<String, List<Integer>> m1 = new HashMap<String, List<Integer>>();
// Java 7+ ダイヤモンド演算子 <> で右辺の型推論
Map<String, List<Integer>> m2 = new HashMap<>();
// Java 8+ メソッド呼び出しでの型推論強化
List<String> empty = Collections.emptyList(); // 推論で <String> 確定
// Java 10+ var で更に簡潔
var m3 = new HashMap<String, List<Integer>>();
C# Generics との比較
| 項目 | Java | C# |
|---|---|---|
| 実装方式 | 型消去 (Type Erasure) | 具現化 (Reification) |
| 実行時の型情報 | 消えている | 残っている (typeof(T)) |
new T() | 不可 | 可能 (new() 制約) |
| プリミティブ型 | 不可 (Integer などラッパー必須) | 可能 (List<int>) |
| 共変・反変 | ワイルドカード ? extends / ? super | out T / in T |
FAQ
Q: List<Object> と List<?> は何が違う?
A: List<Object> は「Object のリスト」専用で List<String> を代入できない。List<?> は「何らかの型のリスト」で任意の List<X> を受け取れる。
Q: raw type の警告 "unchecked" が出る
A: 旧 API との互換用に型パラメータ無しの List を使うと出る警告。可能な限り List<?> や具体型に置き換えること。
Q: @SuppressWarnings("unchecked") はいつ使う?
A: 型消去で避けられないキャスト(リフレクション利用時など)に限り、最小スコープで使う。多用は型安全を損なう兆候。