ジェネリクスを一言でいうと
ジェネリクス(generics)は、
「クラスやメソッドの中に “型の穴” を開けておいて、使うときにその穴に好きな型をはめられる仕組み」です。
List<String> や Map<Integer, String> のように、クラス名のあとに <...> を書きますよね。
この <String> や <Integer, String> がまさに「型パラメータ」であり、ジェネリクスの正体です。
ポイントは、「何でも入る」よりも、「何が入るかをコンパイル時に決める」ことで、
バグを早めに潰し、キャストだらけの危険なコードから解放されることです。
ジェネリクスがない世界をまず体感する
すべて Object にする不安な世界
ジェネリクスが導入される前(Java 5 以前)は、コレクションはこんな使い方しかできませんでした。
import java.util.ArrayList;
import java.util.List;
public class BeforeGenerics {
public static void main(String[] args) {
List list = new ArrayList(); // 型パラメータなし(raw型)
list.add("hello");
list.add("world");
list.add(123); // これもコンパイルOK
Object o = list.get(0); // 戻り値の型は Object
String s = (String) o; // 自分でキャスト
System.out.println(s.toUpperCase());
}
}
Javaここで見えてくる問題は二つあります。
一つ目は、「String のリストにしたいつもりでも、123 みたいな別の型を普通に add できてしまう」こと。
二つ目は、「get の戻り値が常に Object なので、毎回キャストしなきゃいけない」ことです。
そして本当に怖いのは、「変な型を入れてもコンパイルは通ってしまう」ことです。String s = (String) list.get(2); と書いてしまうと、実行時に ClassCastException で落ちます。
つまり、「バグに気づくのが実行してから」になってしまう世界です。
ジェネリクスがある世界:List<String> の意味をちゃんと掴む
List<String> は「String だけを入れるリスト」という約束
同じ例をジェネリクスで書き直します。
import java.util.ArrayList;
import java.util.List;
public class WithGenerics {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
// list.add(123); // これはコンパイルエラー
String s = list.get(0); // 最初から String として返ってくる
System.out.println(s.toUpperCase());
}
}
Javaここで List<String> と書いた瞬間に、「このリストは String 専用です」という宣言になります。
だから list.add(123); のようなコードはコンパイルエラーになり、list.get(0) の戻り値は最初から String 型として扱えるわけです。
つまり、ジェネリクスによって
「入れるときに、変な型を入れようとするとコンパイル時に怒られる」
「取り出すときに、キャストを書かなくてよくなる」
という二つのメリットが同時に得られます。
この「型安全性」と「キャスト不要」が、ジェネリクスの圧倒的な価値です。
「型の穴」を自分で開ける:ジェネリッククラスのイメージ
Object ベースの箱クラスと、その問題
例えば、「何でも一つ入れておける箱クラス」を素直に書くとこうなります。
public class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
Java使う側はこうです。
Box box = new Box();
box.set("hello");
String s = (String) box.get(); // キャスト必須
System.out.println(s.toUpperCase());
Javaここでも、「中身が何型か」はプログラマーの頭の中のルールでしかなく、
コンパイラは何も守ってくれません。別の場所で box.set(123); と書いてしまっても、コンパイルは通ります。
Box<T>:型パラメータ T を持つ箱クラス
これをジェネリクスで書き直すと、こうなります。
public class Box<T> { // <T> が「型パラメータ」
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
Javaここで <T> は「このクラスは T という型パラメータを持ちます」という宣言です。T の名前自体に特別な意味はありませんが、慣習的に Type の T を使うことが多いです。
使うときに、この T に具体的な型をはめ込みます。
public class BoxUsage {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get(); // キャスト不要
System.out.println(s.toUpperCase());
Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer i = intBox.get(); // これもキャスト不要
System.out.println(i + 1);
}
}
Java一つの Box<T> から、Box<String> や Box<Integer> などを「型つきで」作り分けられているのが分かると思います。
ここで大事なのは、「クラスの中身を書き換えずに、扱う型だけ差し替えられる」という感覚です。
メソッドにだけ「型の穴」を開ける:ジェネリックメソッド
クラスではなくメソッドスコープの型パラメータ
今度は、クラス全体じゃなくて「メソッドだけジェネリクスにしたい」場合です。
例えば「配列をそのままリストに移し替える」ユーティリティメソッドを考えてみます。
import java.util.ArrayList;
import java.util.List;
public class GenericsMethodSample {
public static <T> List<T> toList(T[] array) {
List<T> list = new ArrayList<>();
for (T e : array) {
list.add(e);
}
return list;
}
public static void main(String[] args) {
String[] names = {"Alice", "Bob"};
Integer[] nums = {1, 2, 3};
List<String> nameList = toList(names); // T は String と推論される
List<Integer> numList = toList(nums); // T は Integer と推論される
System.out.println(nameList);
System.out.println(numList);
}
}
Javaここで注目してほしいのは、メソッド宣言の「戻り値の型」の前に <T> があることです。
public static <T> List<T> toList(T[] array)
この <T> が、「このメソッド専用の型パラメータ T です」という宣言です。
メソッドの呼び出し時に、引数の型からコンパイラが T を推論してくれるので、
呼ぶ側は普段は <String> など明示しなくて構いません。
「クラス名のあとに <T> が付くのがジェネリッククラス」
「メソッドの戻り値の前に <T> が付くのがジェネリックメソッド」
という形で区別しておくと分かりやすいです。
ダイヤモンド演算子 <> と型推論
new ArrayList<String>() が new ArrayList<>() で済む理由
ジェネリクスを使っていると、こういう書き方をよく見ると思います。
List<String> list = new ArrayList<>();
Java右側に <String> が書いてありませんが、これは Java 7 以降で導入された「ダイヤモンド演算子」のおかげです。
本来はこう書いてもいいところを、
List<String> list = new ArrayList<String>();
Java型が重複して読みにくいので、コンパイラが左辺を見て右側の型パラメータを補完してくれます。
つまり、
「左側で List<String> と書いてあるから、右側の ArrayList<> も中身は <String> だよね」
と Java が察してくれている感じです。
これによって、new HashMap<String, Integer>() が new HashMap<>() と書けるようになり、
ジェネリクスのコードの見通しがかなり良くなります。
「ジェネリクスはコンパイル時の仕組み」という本質
実行時には List<String> と List<Integer> の差は消える(型消去)
ここは少し深めの話ですが、ジェネリクスの理解には重要です。
Java のジェネリクスは「型消去(type erasure)」という仕組みで実装されています。
ざっくりいうと、
コンパイル時:List<String> と List<Integer> は別々の型として扱われる
実行時:どちらも単なる List として扱われ、型パラメータの情報は消えている
ということです。
その結果、例えば次のようなことはできません。
List<String> list = new ArrayList<>();
if (list instanceof List<String>) { // コンパイルエラー
}
Javainstanceof の右側には「型付きの List」は書けません。
実行時には List<String> なのか List<Integer> なのか区別が付かないからです。
書けるのは list instanceof List までです。
また、「T 型の配列」を直接 new することもできません。
public class Sample<T> {
// T[] array = new T[10]; // コンパイルエラー
}
Java実行時に T の具体的な型が分からないので、配列は作れない、という制約があるわけです。
初心者としてはまず、
ジェネリクスは「コンパイル時の型チェックを強化する仕組み」
実行時には全部「生の型」に潰されて動いている
くらいの認識を持っておくと、変なところで戸惑わなくて済みます。
raw 型(型パラメータ無し)に注意する
List とだけ書くのは、ほぼ常に「やめた方がいい」
次のような書き方は、古いコードでよく見かけます。
List list = new ArrayList(); // raw 型
Javaジェネリクスを付けずに使う型を「raw 型」と呼びます。
これはジェネリクス導入前との互換性のために残っている書き方ですが、新しく書くコードでは避けるべきです。
raw 型を使うと、ジェネリクスの型安全性がすべて無効化されます。List とだけ書くのは、事実上 List<Object> のような「何でもありのリスト」にするという宣言だからです。
コンパイラも警告を出します(unchecked という警告)。
「ここ、型パラメータ付けてないけど本当にいいの?」というサインなので、
基本的には List<String> や List<Foo> のように、必ず具体的な型を付ける癖をつけてください。
初心者がまず押さえるべきジェネリクスの本質
ジェネリクスは、記号やルールが多くて「難しそう」に見えがちですが、
本質をシンプルな言葉にするとこんな感じです。
同じクラス・メソッドを、「中で扱う型だけ変えて再利用する」ための機能。
コレクションに「何を入れていいのか」をコンパイル時に縛ることで、バグを早期に防ぐ仕組み。
取り出すときに (String) みたいな危険なキャストを書かなくて済むようにするためのもの。
特に重要なのは、「コンパイル時に型をチェックできる」点です。List<String> に Integer を入れようとした瞬間にエラーになってくれるから、
起動してから ClassCastException で落ちるような「遅い失敗」を減らせます。
どこからジェネリクスに慣れていけばいいか
いきなり「境界付きワイルドカード」や「PECS」みたいな応用に飛び込む必要はありません。
まずは、次のようなところから手を動かして慣れていくのがおすすめです。
コレクションを、必ずジェネリクス付きで宣言してみる(List<String> / Map<String, Integer>)。
自分で Box<T> のような「なんでも入る箱」をジェネリクス化してみる。
簡単なジェネリックメソッド(<T> T first(T[] array) のようなもの)を書いてみる。
この段階だけでも、「型の穴を開けて、使うときに具体的な型をはめ込む」感覚はかなり身につきます。

