Javaの「ジェネリック」と呼ばれる機能は、型安全性を提供し、同時にコードの再利用性を高めるための強力なツールです。この機能を使用すると、コンパイル時に型の整合性が確認できるため、ランタイムでのエラーを減らすことができます。ジェネリックは、クラスやメソッドの定義において型をパラメータとして渡すことを可能にし、特にコレクションフレームワークでよく利用されます。この記事では、Javaの「ジェネリック」の基本から応用までを、完全かつ包括的に解説します。
1. ジェネリックの基本
ジェネリックは、型パラメータを使用してクラスやメソッドを定義します。これにより、同じクラスやメソッドを異なる型に対して再利用できるようになります。ジェネリックを使用すると、型に関する安全性が保証され、コンパイル時に型の不一致が検出されます。

1.1. ジェネリッククラスの定義
ジェネリッククラスは、クラス名の後に型パラメータを追加して定義されます。型パラメータは通常、単一の大文字のアルファベット(T、E、K、Vなど)を使用します。
java// Tは型パラメータとして定義されている
public class Box {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
上記の例では、Box
クラスはジェネリッククラスで、T
という型パラメータを受け取ります。このクラスは、任意の型を扱うことができ、set
メソッドとget
メソッドで値の保存と取得が可能です。
1.2. ジェネリックメソッドの定義
メソッド内でもジェネリックを使用できます。メソッドの型パラメータは、メソッドの前に定義されます。
javapublic void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
この例では、printArray
メソッドはジェネリックメソッドとして定義されており、任意の型の配列を受け取ることができます。
1.3. ジェネリックの使用
ジェネリッククラスやメソッドを使う際には、型を指定してインスタンスを作成したり、メソッドを呼び出したりします。
java// Integer型の値を扱うBoxインスタンスを作成
Box intBox = new Box<>();
intBox.set(10);
System.out.println(intBox.get()); // 10が出力される
// String型の値を扱うBoxインスタンスを作成
Box strBox = new Box<>();
strBox.set("Hello");
System.out.println(strBox.get()); // "Hello"が出力される
2. ワイルドカード(?)の使用
ジェネリックでは、ワイルドカード(?
)を使って、型の範囲を指定することができます。これにより、より柔軟な型の操作が可能になります。
2.1. 上限付きワイルドカード(? extends T
)
上限付きワイルドカードは、指定した型またはそのサブクラスに対して操作を行う場合に使用します。
javapublic void printNumbers(List extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
このメソッドは、Number
型またはそのサブクラス(Integer
、Double
など)のリストを受け取ることができます。これにより、数値型の異なるリストを一つのメソッドで処理できるようになります。
2.2. 下限付きワイルドカード(? super T
)
下限付きワイルドカードは、指定した型またはそのスーパークラスに対して操作を行う場合に使用します。
javapublic void addNumbers(List super Integer> list) {
list.add(10);
}
このメソッドは、Integer
型またはそのスーパークラス(Number
、Object
など)のリストに値を追加することができます。
3. ジェネリックの制約
ジェネリックは型パラメータに対して制約を付けることができます。これにより、指定された型またはそのサブクラスに対してのみ操作を行えるようにすることができます。
3.1. 上限の制約(extends
)
型パラメータに対して上限を設定することで、その型のサブクラスでのみ使用できるように制限できます。
javapublic extends Number> void printNumber(T number) {
System.out.println(number);
}
このメソッドは、Number
型またはそのサブクラス(Integer
、Double
など)の型のみを受け入れます。
3.2. 複数の上限(extends
を使って複数の制約)
Javaのジェネリックでは、複数の上限を設定することも可能です。この場合、&
演算子を使用して制約を連結します。
javapublic extends Number & Comparable> void printComparable(T obj) {
System.out.println(obj.compareTo(obj));
}
この例では、型パラメータT
はNumber
型かつComparable
インターフェースを実装している必要があります。
4. ジェネリックを使ったコレクションの利用
ジェネリックは、Javaのコレクションフレームワークと密接に関連しています。リストやセットなどのコレクションを型安全に扱うためにジェネリックを使用することが推奨されます。
4.1. ジェネリックリストの利用
javaList list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
for (String fruit : list) {
System.out.println(fruit);
}
ここでは、List
の型をString
に指定することによって、リストに追加できる要素の型を制限し、型安全性を確保しています。
4.2. ジェネリックマップの利用
javaMap map = new HashMap<>();
map.put("Apple", 1);
map.put("Banana", 2);
for (Map.Entry entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
ジェネリックマップでは、キーと値の型を指定することによって、型安全なマップ操作ができます。
5. ジェネリックの型消去(Type Erasure)
Javaのジェネリックは、コンパイル時に型が決定され、実行時には型情報が消去されるという特徴を持っています。これを「型消去(Type Erasure)」と呼びます。型消去により、ジェネリックは実行時に実際の型情報を保持しません。
例えば、以下のコードでBox
とBox
はコンパイル時には異なる型として認識されますが、実行時にはどちらもBox
型として扱われます。
javaBox strBox = new Box<>();
Box intBox = new Box<>();
実行時には、strBox
とintBox
はどちらもBox
型であり、実際の型情報は消去されます。このため、実行時に型情報にアクセスすることはできません。
結論
Javaのジェネリックは、型安全性を提供し、コードの再利用性を高めるために非常に強力な機能です。ジェネリックを使うことで、型に関するエラーをコンパイル時に発見でき、ランタイムでのエラーを防ぐことができます。また、ジェネリックを使って柔軟かつ型安全なデータ構造を作成できるため、Javaのプログラミングにおいて重要な役割を果たします。