1. 「生の」コレクション(raw types)の問題
少し歴史を振り返りましょう。Java 5 以前のコレクションは「何でも受け付ける」存在でした。Object 型のオブジェクトを保持し、コンパイラはそこに何を入れるかをチェックしません。文字列を入れる? どうぞ。数値? もちろん。ネコ? それも可能です。
// 「生の」コレクション(raw types)の例、Java 5 以前
List list = new ArrayList();
list.add("こんにちは");
list.add(42);
list.add(new Object());
問題は取り出して使うときに表面化します:
String s = (String) list.get(0); // OK、これは文字列
String s2 = (String) list.get(1); // ドカン! ClassCastException
コンパイラは黙ったまま、ランタイムで ClassCastException が発生します。まるで「りんご」と書かれた箱の中に、カップとバナナとハリネズミが入っているようなものです。
なぜ良くないのか?
- エラーは実行時にしか発覚しません。
- 型が混乱します: 必要な型へ手作業でキャストする必要があります。
- コードの可読性と安全性が低下します。
解決策 — ジェネリクス(総称型)
Generics(ジェネリクス)は、型パラメータを持つクラス、インターフェイス、メソッドを作成できる仕組みです。つまり、コレクションに「文字列だけを保存して」と指示でき、コンパイラがそれを厳密に監視します。
List<String> words = new ArrayList<>();
words.add("こんにちは");
words.add("世界");
// words.add(42); // コンパイルエラー!List<String> に int は追加できない
これでコンパイラは文字列以外をリストに入れさせません。エラーはプログラムを起動する前に捕捉されます。
ジェネリクスの主眼:
コレクション(に限らず)を型安全にし、エラーをランタイムではなくコンパイル時に検出すること。
2. ジェネリクスの文法: コードではどう書くか
山括弧で型を指定する
コレクションを作成するときは、<> の中に要素の型を指定します:
List<String> names = new ArrayList<>();
names.add("アリス");
names.add("ボブ");
// names.add(123); // エラー: 文字列のリストに数値は追加できない
String first = names.get(0); // キャストは不要!
定番の形:
- List<String> — 文字列のリスト
- List<Integer> — 整数のリスト
- Set<Double> — Double の集合
- Map<String, Integer> — キーは String、値は Integer
なぜ単に List と書いてはいけないのか?
書けますが、ジェネリクスの利点をすべて失い、コンパイラが警告します:
List list = new ArrayList(); // raw type — 推奨されません!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // ClassCastException の登場!
現代の Java コードは常にジェネリクスを使います。
ダイアモンド演算子 <>
Java 7 以降は、右辺の型が文脈から分かる場合は省略できます:
List<String> list = new ArrayList<>(); // コンパイラがここは <String> だと推論してくれる
3. 各種コレクションでのジェネリクス
List、Set、Map の例
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("アリス");
uniqueNames.add("ボブ");
Map<String, Integer> ages = new HashMap<>();
ages.put("アリス", 23);
ages.put("ボブ", 31);
独自クラスの例
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. 役立つニュアンス
ジェネリクスの利点
型安全性。 コンパイラが、コレクションに正しい型の要素だけが入るように見張ります。
キャスト不要。 以前: String s = (String) list.get(0);。今は: String s = list.get(0);。
より読みやすく堅牢。 ランタイムのサプライズが減ります。
ジェネリクスの制約
プリミティブ型は使えません。 ジェネリクスはオブジェクトに対してのみ機能し、プリミティブ(int、double、boolean)には使えません。ラッパークラスを使いましょう: Integer、Double、Boolean。
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int は自動的に Integer に変換される(オートボクシング)
型消去(type erasure)の概要
Java のジェネリクスは型消去という仕組みで実装されています。コンパイル後、型パラメータの情報は消去され、ランタイムの JVM はそれが List<String> だったのか単なる List だったのかを知りません。これは後方互換性のためです。
帰結: 具体的な型引数を伴う instanceof によるチェックはできません。
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // コンパイルエラー!
異なる型の要素を追加しようとするとコンパイルエラー
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // コンパイルエラー: incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("ネコ", 5);
// map.put(3, "ゾウ"); // エラー: キーは String、値は Integer でなければならない
そしてこれは素晴らしいことです: エラーはコンパイル時に捕捉されます。
コレクションだけではない
ジェネリクスは自作のクラスやメソッドでも使えます。例えば、汎用の「箱」:
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("こんにちは");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
コレクションではジェネリクスが標準ですが、Stream API や Optional など他の場所でも目にするでしょう。
5. ジェネリクスでよくある誤り
誤り1: 「生の」コレクションの使用。 List list = new ArrayList(); のような書き方は型安全性を失わせます。常に型パラメータを指定しましょう。例: List<String>。
誤り2: プリミティブ型を使おうとする。 List<int> のようには書けません。List<Integer> を使いましょう。
誤り3: コレクションから読むたびに手動キャストする。 ジェネリクスを使っていれば、(String) list.get(i) のようなキャストは不要です。必要になるなら、どこかで型を破っています。
誤り4: 型引数が実行時に利用できると期待する。 型消去のため、instanceof で List<String> のようにチェックすることはできません。
誤り5: 1 つのコレクションに異なる型を混在させる。 List<String> と宣言したなら、Integer を追加してはいけません。コンパイラが弾いてくれますし、それで良いのです。
GO TO FULL VERSION