SJ blog
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>

TSomeClass のサブタイプであることを保証します。

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 はコードの再利用性と型安全性を同時に高める強力な機能です。最初は難しく感じますが、コレクションを使う時点で毎日使っています。