はじめに
Javaでリストやマップを定義する際、基本的にこのような書き方をすると思います。
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
この <String> や <String, Integer> の部分がジェネリクス(Generics) です。
「なんとなく使っているけど、仕組みはよくわからない」という方も多いと思います。この記事では、ジェネリクスをなぜ必要なのかという理由から順番に解説します。
ジェネリクスがなかった時代の問題
まず、ジェネリクスが登場する前(Java 5以前)のコードを見てみましょう。
// ジェネリクスなし(古いコード)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 文字列も整数も混在できてしまう!
String value = (String) list.get(0); // キャストが必要
String error = (String) list.get(1); // → ClassCastException!実行時エラー
この書き方には2つの大きな問題があります。
- どんな型でも入れられてしまう(意図しないデータが混入する)
- 取り出すたびにキャストが必要で、型が違うと実行時にエラーになる
つまり、バグがコンパイル時ではなく実行時に発覚するという危険な状態でした。
ジェネリクスで何が解決するのか
ジェネリクスを使うと、「このコレクションには〇〇型しか入れられない」 とコンパイル時に明示できます。
// ジェネリクスあり
List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // コンパイルエラー!ここで気づける
String value = list.get(0); // キャスト不要!
ジェネリクスのメリットをまとめると次の通りです。
| ジェネリクスなし | ジェネリクスあり | |
|---|---|---|
| 型の混入 | 実行時まで気づかない | コンパイル時にエラー |
| キャスト | 必要 | 不要 |
| コードの意図 | 不明確 | 明確 |
基本的な使い方
コレクションでの使い方
// String型のみ入れられるList
List<String> names = new ArrayList<>();
names.add("田中");
names.add("佐藤");
// names.add(100); // コンパイルエラー
// Integer型のみ入れられるList
List<Integer> scores = new ArrayList<>();
scores.add(85);
scores.add(92);
// String → Integer のMap
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("田中", 85);
scoreMap.put("佐藤", 92);
<> の中に書くもの
<> の中には参照型(クラス)を書きます。プリミティブ型(int, double など)は書けません。
List<int> list = new ArrayList<>(); // コンパイルエラー
List<Integer> list = new ArrayList<>(); // OK(ラッパークラスを使う)
| プリミティブ型 | ラッパークラス |
|---|---|
int |
Integer |
double |
Double |
boolean |
Boolean |
char |
Character |
long |
Long |
自分でジェネリクスを使ったクラスを作る
ジェネリクスはコレクションだけのものではありません。自分でクラスを作るときにも使えます。
ジェネリクスなしで書いた場合の問題
例えば、「値を1つ保持するBox(箱)クラス」を作るとします。
// Stringだけ入れられるBox
class StringBox {
private String value;
public void set(String value) { this.value = value; }
public String get() { return value; }
}
// Integerだけ入れられるBox
class IntegerBox {
private Integer value;
public void set(Integer value) { this.value = value; }
public Integer get() { return value; }
}
型が違うだけで、ほぼ同じコードを2回書いています。型が増えるたびにクラスを増やす必要があり、非常に非効率です。
ジェネリクスで解決する
// ジェネリクスを使ったBox
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
T は型パラメータと呼びます。使うときに具体的な型を指定します。
Box<String> stringBox = new Box<>();
stringBox.set("こんにちは");
String message = stringBox.get(); // キャスト不要
System.out.println(message); // こんにちは
Box<Integer> intBox = new Box<>();
intBox.set(100);
Integer num = intBox.get();
System.out.println(num); // 100
1つのクラスで、どんな型にも対応できるようになりました。
型パラメータの命名慣習
型パラメータには慣習的な名前があります。
| 名前 | 意味 | よく使われる場面 |
|---|---|---|
T |
Type(型) | 汎用的な型パラメータ |
E |
Element(要素) | コレクションの要素 |
K |
Key(キー) | Mapのキー |
V |
Value(値) | Mapの値 |
R |
Return(戻り値) | メソッドの戻り値の型 |
// JavaのListインターフェースはこのように定義されている(イメージ)
interface List<E> {
void add(E element);
E get(int index);
}
// JavaのMapインターフェースはこのように定義されている(イメージ)
interface Map<K, V> {
void put(K key, V value);
V get(K key);
}
ジェネリクスメソッド
クラス全体ではなく、特定のメソッドだけにジェネリクスを使うこともできます。
public class Utils {
// どんな型のListでも最初の要素を返すメソッド
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
}
List<String> names = List.of("田中", "佐藤", "鈴木");
String first = Utils.getFirst(names);
System.out.println(first); // 田中
List<Integer> scores = List.of(85, 92, 78);
Integer topScore = Utils.getFirst(scores);
System.out.println(topScore); // 85
型の制限をつける(境界型パラメータ)
「T はどんな型でもいいが、Number のサブクラスに限る」という制限をつけることができます。
// T は Number またはそのサブクラスに限定
public static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T num : list) {
total += num.doubleValue();
}
return total;
}
List<Integer> intList = List.of(1, 2, 3, 4, 5);
System.out.println(sum(intList)); // 15.0
List<Double> doubleList = List.of(1.5, 2.5, 3.0);
System.out.println(sum(doubleList)); // 7.0
List<String> strList = List.of("a", "b");
// sum(strList); // コンパイルエラー!StringはNumberのサブクラスではない
extends を使うことで「特定の型の範囲に絞り込む」ことができます。
ワイルドカード(?)
ワイルドカードは「型が何であるか不明」を表す ? です。
// どんな型のListでも受け取れる(読み取り専用のイメージ)
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printList(List.of("田中", "佐藤")); // OK
printList(List.of(1, 2, 3)); // OK
printList(List.of(true, false)); // OK
上限境界ワイルドカード(? extends)
? extends T 型は、Tもしくはそのサブクラスを全て表す型 を意味します。
// Number またはそのサブクラスのList(読み取り用)
public static double sumWildcard(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
下限境界ワイルドカード(? super)
? super T 型は、Tもしくはそのスーパークラス全てを表す型 を意味します。
// Integer またはそのスーパークラスのList(書き込み用)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
よくある疑問
Q. List<Object> と List<?> は何が違う?
List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();
// objList = strList; // コンパイルエラー!
// List<Object> と List<String> は別の型
List<?> wildcardList;
wildcardList = strList; // OK!ワイルドカードなら代入できる
List<Object> は「Object 型の要素を持つList」であり、List<String> とは別の型です。ワイルドカード List<?> を使うと、どんな型パラメータのListも受け取れます。
Q. 実行時に型情報はどうなる?
Javaのジェネリクスは型消去(Type Erasure) という仕組みを使っており、コンパイル後の .class ファイルには型情報が残りません。
// コンパイル前
List<String> list = new ArrayList<String>();
// コンパイル後(バイトコードのイメージ)
List list = new ArrayList();
これはJava 5以前のコードとの後方互換性を保つための設計です。
まとめ
ジェネリクスを使うと
- 型安全になる(意図しない型の混入をコンパイル時に検知)
- キャストが不要になる(コードがすっきりする)
- 再利用性が上がる(1つのクラスで複数の型に対応できる)
| 概念 | 書き方 | 意味 |
|---|---|---|
| 型パラメータ | <T> |
使う型を外から指定する |
| 境界型パラメータ | <T extends Number> |
型の範囲を制限する |
| ワイルドカード | <?> |
型が不明であることを示す |
| 上限境界ワイルドカード | <? extends Number> |
Numberかそのサブクラス |
| 下限境界ワイルドカード | <? super Integer> |
IntegerかそのスーパークラスA |
最初は List<String> のような使い方から始めて、慣れてきたら自分でジェネリクスを使ったクラスやメソッドを書いてみましょう。Java Gold試験でも頻出テーマなので、理解を深めておくと試験対策にもなります。