ジェネリックス(Generics)
ジェネリックスは、クラスやメソッドで使用するデータ型をコンパイル時に指定できる機能です。これにより、型の安全性を保証し、型変換を最小限に抑えることができます。
ジェネリックスを使用する理由
-
型の安全性の保証
コンパイル時に型をチェックすることで、ランタイム時の型エラーを防ぐことができます。 -
型変換の不要
以前はObject型を使用してダウンキャストする必要がありましたが、ジェネリックスを使用すると不要になります。 -
再利用性の向上
様々な型に対応できる汎用的なコードを記述できます。
ジェネリックスの使い方
1. ジェネリックスを使ったクラス
class Box<T> { // <T> : 型パラメータ
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>(); // T → String に指定
stringBox.set("Hello");
System.out.println(stringBox.get()); // Hello
Box<Integer> intBox = new Box<>(); // T → Integer に指定
intBox.set(100);
System.out.println(intBox.get()); // 100
}
}
2. ジェネリックスを使ったメソッド
class Util {
public static <T> void print(T value) {
System.out.println(value);
}
}
public class Main {
public static void main(String[] args) {
Util.print("Hello"); // String
Util.print(123); // Integer
Util.print(3.14); // Double
}
}
3. 境界型パラメータ(<T extends Number>)
ジェネリックスの型パラメータTの範囲を制限することができます。
class NumberBox<T extends Number> { // Number またはそのサブクラスのみ許可
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
public class Main {
public static void main(String[] args) {
NumberBox<Integer> intBox = new NumberBox<>(); // OK
NumberBox<Double> doubleBox = new NumberBox<>(); // OK
// NumberBox<String> strBox = new NumberBox<>(); // コンパイルエラー!
}
}
📌 特徴
-
TはNumberまたはそのサブタイプに限定されます。 -
Tは固定された単一の型であるため、setValue()でT型の値を追加可能です。
4. ワイルドカード(?)
上限境界ワイルドカード(<`? extends T`>)
TまたはTのサブタイプを許可(読み取り専用)
コレクションを使用する際に、読み取り専用として制限する場合に使用します。
class Printer {
public static void printList(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
}
public class Main {
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
Printer.printList(intList); // OK
Printer.printList(doubleList); // OK
}
}
📌 特徴
-
<? extends Number>はNumberのサブタイプを受け入れます。 -
List<? extends Number>はList<Integer>やList<Double>などを受け取れます。 - 正確な型が不明なため、値の追加は不可です。
値の追加(書き込み)が不可能な詳しい理由
コンパイラはリストの具体的な型がTのサブタイプのどれなのかを特定できません。
List<? extends Number> list = new ArrayList<Integer>();
list.add(10); // ❌ コンパイルエラー!
listは<? extends Number>のため、どのサブタイプが来るか分かりません。
例えばlist = new ArrayList<Double>();となる場合もあります。
もしlist.add(10);を許可すると、listがArrayList<Double>の時に異なる型(Integer)を追加することになります。
この問題を防ぐためにJavaは値の追加を禁止しています。
ただしnullの追加は可能です。
List<? extends Number> list = new ArrayList<Integer>();
list.add(null); // ✅ OK
nullはどの型にもなれるため、例外的に許可されます。
下限境界ワイルドカード(<? super T>)
TまたはTの親クラスを許可(書き込み可能)
class Adder {
public static void addNumber(List<? super Integer> list) {
list.add(10); // OK
}
}
public class Main {
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
Adder.addNumber(numList); // OK
List<Object> objList = new ArrayList<>();
Adder.addNumber(objList); // OK
}
}
リストに値を追加できますが、値を読み取る際にはObject型として取得する必要があります。
値をObjectとして読み取る理由
Object obj = numList.get(0);
numList.get(0);の戻り値の型はObjectです。
理由は、<? super Integer>がIntegerよりも上位の型になる可能性があるためです。
例えば、List<Number>の場合、Number型の値が含まれている可能性があリます。
Javaは型の安全性を保証する必要があるため、最も確実な型であるObjectを返すようになっています。
ジェネリックス型制限 VS 上限境界ワイルドカード
形が似たような感じで、何が違うのかわかりやすく表で整理しました。
| 比較項目 |
ジェネリックス型制限 (<T extends Number>) |
上限境界ワイルドカード (? extends Number) |
|---|---|---|
| 値の追加可否 | ✅ T 型の追加可能 |
❌ 追加不可 |
| 値の読み取り可否 | ✅ T 型として読み取り可能 |
✅ Number 型として読み取り可能 |
| 使用目的 | 型を確定的に制限 | 特定の型を読み取り専用で使用 |