Javaのジェネリクス:型安全なコレクションと通配符の実践

ジェネリクスの基本概念

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以降の「ダイヤモンド演算子」<>により、型引数の冗長な記述が不要になりました。

ワイルドカードの上限・下限は、コレクションの「読み出し」か「書き込み」かという使用目的に応じて選択することで、型安全性と柔軟性の両立が可能になります。

タグ: Java ジェネリクス ワイルドカード 型安全 コレクション

8月3日 16:29 投稿