ジェネリクスの基本概念
Javaのコレクションは初期段階ではすべてのオブジェクトをObject型として扱いました。このため、要素を取り出すたびに明示的なキャストが必要になり、型不一致によるClassCastExceptionのリスクが高まりました。ジェネリクスは、コンパイル時に型安全性を保証し、ランタイムでのキャストエラーを回避する仕組みです。
ジェネリッククラスの定義
ジェネリッククラスは、型パラメータをクラス定義に含めることで、インスタンス生成時に具体的な型を指定します。
public class Box<T> {
private T content;
public void put(T item) {
this.content = item;
}
public T get() {
return content;
}
}
使用例:
Box<String> stringBox = new Box<>();
stringBox.put("Hello");
String message = stringBox.get(); // キャスト不要
Box<Integer> intBox = new Box<>();
intBox.put(42);
int value = intBox.get(); // 型安全に取得
このとき、コンパイラはBox<String>を「String専用のボックス」として処理し、put(Integer)のような型ミスマッチをコンパイルエラーとして検出します。
ジェネリックメソッド
メソッド単位で型パラメータを定義することで、クラスのジェネリクスとは独立して型を制御できます。
public class Utility {
// メソッドごとに型Tを決定
public <T> T copy(T source) {
return source; // 例として単純返却
}
public <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
}
呼び出し時の型推論:
Utility util = new Utility();
String copied = util.copy("Java"); // T = String
Integer result = util.copy(100); // T = Integer
double total = util.sum(3.14, 5); // T = Double(自動型変換)
ジェネリックインターフェース
インターフェースにも型パラメータを導入でき、実装クラスで型を固定または引き継ぎます。
public interface Repository<K, V> {
void save(K key, V value);
V find(K key);
}
型を固定する実装:
public class StringIntRepository implements Repository<String, Integer> {
@Override
public void save(String key, Integer value) { /* 実装 */ }
@Override
public Integer find(String key) { /* 実装 */ }
}
型を引き継ぐ実装(ジェネリッククラス):
public class GenericRepository<K, V> implements Repository<K, V> {
private final Map<K, V> storage = new HashMap<>();
@Override
public void save(K key, V value) {
storage.put(key, value);
}
@Override
public V find(K key) {
return storage.get(key);
}
}
静的メソッドとジェネリクス
静的メソッドはインスタンスの型パラメータを参照できません。そのため、静的ジェネリックメソッドは独自の型パラメータを宣言する必要があります。
public class Container<T> {
// NG:クラスのTは静的文脈で使えない
// public static T getStaticValue() { ... }
// OK:静的メソッド用に独立した型パラメータを宣言
public static <U> U create(U value) {
return value;
}
}
ワイルドカード:型の柔軟な制御
ジェネリクスの型引数が不確定な場合、?(ワイルドカード)を使って柔軟な型指定が可能です。
無制限ワイルドカード
public static void printAll(Collection<?> items) {
for (Object item : items) {
System.out.println(item);
}
}
// 使用例
List<String> strings = List.of("a", "b", "c");
List<Integer> numbers = List.of(1, 2, 3);
printAll(strings); // OK
printAll(numbers); // OK
ただし、ワイルドカードを使用したコレクションには要素を追加できません(add()はコンパイルエラー)。
上限ワイルドカード(? extends T)
指定型およびそのサブタイプのみを受け入れます。読み取り専用のシナリオに適します。
public static double sumNumbers(Collection<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// 使用例
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
List<String> strs = List.of("a", "b");
sumNumbers(ints); // OK
sumNumbers(doubles); // OK
sumNumbers(strs); // エラー:StringはNumberのサブタイプではない
下限ワイルドカード(? super T)
指定型およびそのスーパー型のみを受け入れます。書き込み専用のシナリオに適します。
public static void addIntegers(Collection<? super Integer> target, Integer value) {
target.add(value); // OK:Integerは? super Integerの範囲内
}
// 使用例
Collection<Integer> intColl = new ArrayList<>();
Collection<Number> numColl = new ArrayList<>();
Collection<Object> objColl = new ArrayList<>();
addIntegers(intColl, 10); // OK
addIntegers(numColl, 20); // OK
addIntegers(objColl, 30); // OK
addIntegers(List.of(1,2,3), 40); // エラー:List<Integer>は不変
型安全と型推論のバランス
ジェネリクスは、コンパイル時型チェックを強化することで、ランタイムエラーを未然に防ぎます。また、Java 7以降の「ダイヤモンド演算子」<>により、型引数の冗長な記述が不要になりました。
ワイルドカードの上限・下限は、コレクションの「読み出し」か「書き込み」かという使用目的に応じて選択することで、型安全性と柔軟性の両立が可能になります。