ジェネリッククラスを作る
このレッスンで分かること
class Box<T>の<T>は「中身の型を後から決める」プレースホルダTはType、EはElement、K/VはKey/Valueの命名慣習- プリミティブ型 (
int等) は型パラメータに使えない。Integerなどのラッパー型を使う- 実行時には
型消去でBox<String>もBox<Integer>も同じBoxクラスになる
ジェネリッククラスを作る とは
型パラメータ
<T>を使って、任意の型を受け取れるジェネリッククラスBox<T>を自分で定義し、文字列を包んで取り出すコードを書いてみよう。
「型を後から決める」仕組み ─ ジェネリクスとは?
前のレッスンで ArrayList<Integer> や ArrayList<String> のように、山かっこの中に型を書く ジェネリクス という記法を見ました。あの <...> の正体は、「このクラスは中に入れる値の型を、後から指定して使うよ」というプレースホルダです。Java の標準ライブラリには、こうした「中に入れる型を呼び出し側が決めるクラス」が大量に登場します。
ジェネリクスは Java 5 (2004 年) で導入された仕組みです。それより前は
Listの中に何でも入れられて、取り出すたびにキャストが必要、しかも実行時にエラーが出る、というつらい時代がありました。ジェネリクスのおかげで、コンパイル時に「この箱にはStringしか入りません」と保証できるようになりました。
ArrayList<E> の E は Element (要素) の頭文字で、「ここに後から具体的な型が入る」という枠です。同じ仕組みで Map<K, V> の K は Key、V は Value を表しています。これらは Java 開発者の間で広く使われている命名の慣習で、自分で書くときも倣うと読みやすくなります。
本章では、自分で ジェネリッククラス を作る側を体験します。中に入れる型を 1 つの記号で表しておき、new Box<String>() のように呼び出し側で実体を決めてもらう、というスタイルに慣れていきましょう。
ArrayList<E> で見るジェネリクスの便利さ
まずは標準ライブラリの ArrayList<E> のソースコード (概念だけ) を眺めてみます。実際の JDK 内では次のような形で定義されています。
Java
public class ArrayList<E> {
private Object[] elements;
private int size;
public void add(E value) {
// 末尾に value を入れる
}
public E get(int index) {
// index 番目を E 型で返す
return (E) elements[index];
}
}注目したいのは、add の引数の型と get の戻り値の型が両方とも E になっている点です。ArrayList<String> として使うと、E が String に置き換わったのと同じ振る舞いをするので、add("hello") は OK、add(123) はコンパイルエラーになります。String s = list.get(0); のように、取り出した値もそのまま String として受け取れます。
ジェネリクスを使わずに
Object[]とObjectを使って同じことをすると、addには何でも入ってしまい、getの戻り値はObjectなので毎回キャストが必要になります。コンパイル時の安全性と読みやすさが大きく違うのが分かります。
ジェネリッククラスを自分で定義する
それでは、今回の主役 Box<T> クラスを書いてみます。中身は「何か 1 つ値を抱える箱」というシンプルなものです。
Java
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return this.value;
}
}ポイントは次のとおりです。
- クラス名の後ろに
<T>を書く ─ ここで「このクラスは型パラメータTを 1 つ持つよ」と宣言している - フィールド
valueの型にTを使う ─ 何が来るかは呼び出し側が決める - コンストラクタの引数も
Tで受け取る ─ 渡されたものをそのまま入れる getValueの戻り値もT─ 入れたときの型のまま返せるので、利用側でキャスト不要
呼び出し側のコードはこうなります。
Java
Box<String> stringBox = new Box<>("hello");
String s = stringBox.getValue(); // String として取り出せる
System.out.println("Box[" + s + "]"); // Box[hello]
Box<Integer> intBox = new Box<>(42);
Integer n = intBox.getValue(); // Integer として取り出せる
System.out.println("Box[" + n + "]"); // Box[42]new Box<>("hello") の右側のかっこが空っぽなのは、ArrayList のときと同じ ダイヤモンド演算子 です。左側で Box<String> と書いているので、右側はコンパイラが自動的に推論してくれます。
型パラメータの命名規則
型パラメータの名前は理屈の上では何でも構いませんが、Java の世界には次のような強い慣習があります。
T─Type、汎用の何か 1 つの型 (最初に出すならこれ)E─Element、コレクションの要素 (ArrayList<E>Set<E>など)KV─KeyValueのペア (Map<K, V>HashMap<K, V>)R─Result、関数型インタフェースの戻り値 (Function<T, R>)N─Number、数値系の型パラメータ
1 文字大文字、というのが共通ルールです。<MyType> のように長い名前にしてしまうと、コードを読む人が「これはクラス? インタフェース? それとも型パラメータ?」と一瞬迷います。慣習に従っておくのが無難です。
慣習はあくまで慣習で、Java の文法上は
class Container<Anything>でも動きます。とはいえチーム開発では「全員が一目で分かる」ことが大事なので、TEKVを素直に使うのが安全です。
Mermaid で見るジェネリクスの実体化
Box<T> というクラスは 1 つしかありませんが、Box<Integer> と Box<String> のように違う型で使うと、まるで別々のクラスのように振る舞います。中身のフィールドや戻り値の型が、それぞれ Integer と String に置き換わるからです。
図のように、1 つの設計図 Box<T> から、複数の「型が確定したクラス」が型ごとに展開されるイメージを持っておくと、ジェネリクスの感覚がつかみやすくなります。
よくある間違い
ジェネリッククラスを書き始めると、初学者は同じところでよく詰まります。事前に知っておきましょう。
- raw 型で書いてしまう ─
Box box = new Box("hello");のように<>を省略するとraw 型扱いになり、getValue()の戻り値がObjectになってしまいます。コンパイラは警告を出すだけで止めてはくれませんが、現代の Java では絶対に書かないのが鉄則です - プリミティブ型を
Tに入れようとする ─Box<int>Box<double>はコンパイルエラーです。型パラメータには必ず参照型 (クラス) を渡す必要があるので、intはInteger、doubleはDouble、booleanはBooleanというラッパー型を指定します Tをstaticフィールドに使う ─static T defaultValue;のような書き方はコンパイルエラーになります。staticはクラス全体で共有される領域で、インスタンスごとにTが変わるという前提と矛盾するためです。staticメソッドにジェネリクスを使いたいときは、メソッド側に<T>を書く「ジェネリックメソッド」という別の仕組みを使います
もう 1 つよく聞かれるのが、「Box<String> と Box<Integer> は同じクラス?」という疑問です。実は実行時には Java の 型消去 (type erasure) という仕組みが働いて、Box<String> も Box<Integer> も Box という同じクラスにまとめられます。型情報はコンパイル時のチェックにだけ使われ、実行時には消えるのです。
型消去は Java 5 でジェネリクスを導入したときに、それ以前のJava 4用にコンパイルされたクラスファイルとの互換性を保つために選ばれた設計です。C#やKotlinのように実行時にも型情報を持つ言語と違って、Java のジェネリクスは「コンパイル時の安全性」に特化した仕組みだと覚えておきましょう。
この仕様の影響で、if (obj instanceof Box<String>) のような書き方はできず、if (obj instanceof Box) までしかチェックできません。実用上は気にしない場面が多いですが、リフレクションや高度なライブラリを触るときに思い出すと役に立ちます。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- API レスポンスの共通ラッパー ─
ApiResponse<T>のようにstatuserrorCodeメタ情報と本体Tをまとめる定番パターン - ページネーション結果 ─
Page<T>にList<T> itemsint totalCountを持たせ、Page<User>Page<Order>で使い回す - Result / Either 型の自作 ─
Result<T>に成功時の値か失敗時のエラーを格納し、nullを返す代わりに型で表現する - キャッシュ層 ─
Cache<K, V>のような汎用キャッシュ。社内ライブラリで作っている会社は意外と多い
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
- raw 型を使っている ─
Box box = new Box(...)のように<>を省くのは新規コードでは原則 NG - 型パラメータ名が長すぎる ─
class Box<TheTypeOfValue>のような書き方。1 文字大文字に直すよう求められる staticフィールド / メソッドで型パラメータを使おうとしている ─static T defaultValue;はコンパイルエラー- 不要なキャストが残っている ─ ジェネリクスを使えばキャストは不要、削れるはずとレビューが入る
パフォーマンス考慮事項
- オートボクシングのコスト ─
Box<Integer>を大量に作るとヒープが汚れる。CPU バウンドではint専用クラスのほうが速い - 型消去の影響 ─ 実行時には
Box.class1 つ。instanceof Box<String>のような細粒度チェックはできない - 配列との混在 NG ─
new Box<String>[10]は generic array creation エラー。ArrayList<Box<String>>を使う - 深くネストする型 ─
Map<String, Map<Integer, List<User>>>等は IDE 推論が重くなる。型エイリアスクラスで見通しを上げる
ここまでの要点
クラス名の後ろに <T> で型パラメータを宣言、フィールド / メソッドで T を使う。T/E/K/V の慣習に従い、static フィールドや配列との混在は避ける。
やってみよう
それでは今回の課題に挑戦します。やることは次のとおりです。
Solutionクラスの中で、Box<T>相当のジェネリッククラスを定義する (static class Box<T>でも、別ファイル相当の作りでも OK)- コンストラクタで受け取った値を
valueフィールドに保存し、getValue()で取り出せるようにする Solution.boxedValue(String s)の中でnew Box<String>(s)(もしくはnew Box<>(s)) を作り、getValue()の結果を取り出す- 取り出した文字列を
"Box[" + s + "]"の形に連結して返す
戻り値のフォーマットは Box[hello] のように、Box[ で始まり ] で終わる単純な文字列です。s が空文字 "" の場合は "Box[]" を返してください。Java という入力なら "Box[Java]" です。一度書けたら、Box<Integer> 版も自分で書いてみると、ジェネリクスの「型を変えるだけで同じ作りが使える」便利さを実感できます。
慣れてきたら、Box に setValue(T newValue) を足してみたり、フィールドに値を 2 つ持つ Pair<K, V> クラスを書いてみたりすると、Map.Entry<K, V> などの標準ライブラリで使われている設計の感覚がつかめます。次のレッスンでは、メソッド単位で型パラメータを取る「ジェネリックメソッド」を扱う予定です。
よくある質問
Q. ジェネリクスを使うと何が嬉しいですか?
A. 型安全と再利用性が両立します。Box
Q. ワイルドカード ? extends と ? super の違いは?
A. ? extends T は「T のサブクラス」で読み取り専用、? super T は「T のスーパークラス」で書き込み専用というイメージです。PECS(Producer Extends, Consumer Super)の頭文字で覚えるのが定番です。データを取り出すなら extends、入れるなら super を選びます。
Q. ジェネリクスはコンパイル後も残りますか?
A. Java は型消去(erasure)方式のため、実行時には Object になります。そのため List
次のレッスン
次は ジェネリックメソッド で、ジェネリックメソッド を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- ジェネリッククラス の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. ジェネリッククラス とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Solutionクラスの中 (もしくは同ファイル) に、型パラメータ<T>を持つBox相当のジェネリッククラスを定義することBoxクラスは値を保存するフィールドと、それを取り出すgetValue()相当のメソッドを持つこと (raw 型Boxのままにせず、必ず<>で型を指定すること)boxedValueの内部でBox<String>を生成し、getValue()で取り出した値を使って"Box[" + value + "]"の文字列を返すこと
入出力例
test-cases.txt
boxedValue("hello") → "Box[hello]"
boxedValue("Java") → "Box[Java]"
boxedValue("") → "Box[]"
boxedValue("こんにちは") → "Box[こんにちは]"