2.

Java ジェネリクスの基本と実践(型パラメータ・ワイルドカード・PECS・型消去)

編集
この記事の要点
  • ジェネリクス (Generics)型パラメータを使ってクラスやメソッドを「型に依存しない」形で書く仕組み
  • class Box<T> { T value; } のように <T> を宣言、利用時に Box<String> と具体型を指定
  • 型安全: コンパイル時に型不一致を検出 → 実行時の ClassCastException を防ぐ
  • ワイルドカード: <? extends T>(読み取り用)、<? super T>(書き込み用)— PECS 原則
  • 型消去 (Type Erasure): 実行時には型情報が消える → new T() やインスタンスチェックは不可

ジェネリクスとは何か

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<T>) で型消去を回避

// 実行時に型を知りたい場合は 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 との比較

項目JavaC#
実装方式型消去 (Type Erasure)具現化 (Reification)
実行時の型情報消えている残っている (typeof(T))
new T()不可可能 (new() 制約)
プリミティブ型不可 (Integer などラッパー必須)可能 (List<int>)
共変・反変ワイルドカード ? extends / ? superout 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: 型消去で避けられないキャスト(リフレクション利用時など)に限り、最小スコープで使う。多用は型安全を損なう兆候。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 型変換
  2. ジェネリクス

最近更新/作成されたページ