本文へスキップ
← 記事一覧に戻る

javaのジェネリクスについて

ジェネリクスとは

Javaのジェネリクスはクラスやメソッドで扱うデータ型を、使う側で後から指定できるようにする仕組みです。 一言で言えば、「型をパラメータ化する(引数のように扱う)」機能です。これによって、特定の型に依存しない汎用的なコードを書くことができます。

なぜジェネリクスが必要なのか?

ジェネリクスが登場する前(Java 1.4以前)は、あらゆるオブジェクトを扱える Object 型が使われていました。しかし、それには2つの大きな問題がありました。

  • 実行時のエラー(ClassCastException): 違う型を入れようとしても、コンパイル時には気づけません。
  • キャストの手間: 取り出すたびに (String) のように型変換を書く必要がありました。

ジェネリクスを使うと、コンパイル時に型チェックが行われるため、安全でクリーンなコードになります。

ジェネリクスの設計思想

「型の安全性をコンパイル時に保証し、プログラマの意図(この変数はこの型であるという約束)をコードに明確に反映させる」 という考え方があります。

Type Safety(型安全性)の追求

ジェネリクス以前の Java は、「実行してみるまで型が正しいかわからない」という不安定さを抱えていました。

  • 思想: 「実行時のエラー(ClassCastException)」を「コンパイル時のエラー」に格上げする。
  • メリット: 開発中にミスに気づけるため、本番環境でシステムが突然落ちるリスクを激減させます。

Reusability without Compromise(妥協のない再利用性)

「汎用的な処理(アルゴリズム)」と「具体的なデータ型」を切り離すという思想です。

  • 思想: データの「入れ物」や「並べ替え」といったロジックは型に依存しないはず。ならば、ロジックは1つだけ書き、型は使う時に「注入」すればよい。
  • 対比:
    • ジェネリクスなし:StringList, IntegerList と型ごとにクラスを作る必要がある。
    • ジェネリクスあり:List ひとつで、あらゆる型に対応できる。

Clear Intent / Documentation(意図の明示)

コード自体が「この変数には何が入るべきか」というドキュメントの役割を果たすという思想です。

  • 思想: List ではなく List<User> と書くことで、後からコードを読む人に「ここには User しか入りません」という強いメッセージを伝える。
  • メリット: instanceof で中身を確認したり、コメントで「※このリストには文字列を入れてください」と書く必要がなくなります。
補足

Java のジェネリクスには、 「過去のコードとの互換性を壊さない」 という非常に強い制約(思想)があります。

  • 型消去とは: コンパイルが終わると、<T> などの情報は消え、内部的には Object として扱われます。
  • なぜそうしたか: ジェネリクスが導入された Java 5 以前に作られた膨大なプログラム(List(型指定なし)を使っているコードなど)が、新しい Java でもそのまま動くようにするためです。

現代の設計思想としての位置づけ
最近のプログラミング(特に大規模開発)では、 「人間は間違えるもの」 という前提があります。ジェネリクスは、その人間の間違いをコンピュータが厳密にチェックしてくれる「防護柵」のような役割を担っています。

基本的な使い方

最も身近な例は ArrayList です。

// <String> がジェネリクスによる型指定
List<String> list = new ArrayList<>();
 
list.add("Java");
// list.add(10); // コンパイルエラー(String以外は受け付けない)
 
String text = list.get(0); // キャスト不要

自作クラスでの使用

自分でクラスを作る際、以下のように (Typeの略)などの記号を使って定義します。

// T は「型パラメータ」と呼ばれる
public class Box<T> {
    private T content;
    public void set(T content) {
        this.content = content;
    }
    public T get() {
        return content;
    }
}
// 使うときに具体的な型を指定する
Box<Integer> intBox = new Box<>();
intBox.set(123);

ジェネリクスメソッド

public class Utility {
    // 戻り値の前の <T> が「このメソッドで T を使います」という宣言
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
 
    public static void main(String[] args) {
        String[] strArray = {"A", "B", "C"};
        Integer[] intArray = {1, 2, 3};
 
        // 呼び出す際、引数の型に合わせて T が自動的に決まる
        printArray(strArray); // T は String になる
        printArray(intArray); // T は Integer になる
    }
}
 
public class Utility {
 
    public static void main(String[] args) {
        String str = identity("hoge");
        System.out.println(str);
        int num = identity(10);
        System.out.println(num);
    }
 
    public static <T> T identity(T data) {
        // 何らかの共通処理(ログ出力など)
        System.out.println("Processing: " + data);
        return data; // そのままの型で返す
    }
}

よく使われる記号の慣習

記号 意味(由来) 主な使用例
T Type(型) 最も一般的に使われる(Box など)
E Element(要素) List, Set などのコレクション
K Key(キー) Map<K, V> のキー側
V Value(値) Map<K, V> の値側
N Number(数値) 数値型に限定したい場合
S, U (2つ目、3つ目の型) T 以外に別の型が必要な場合

気づいたこと

  • ジェネリクスを使用するとキャストの手間が減る
  • 様々な型を扱えるためプログラムの再利用がしやすくなる