【Java】Javaのジェネリクスの基本を初心者向けに解説

Java

こんにちは、かつコーチです。
List<String>のように山括弧を使う書き方は、Listの解説記事で何度も見てきたと思います。
今回はこの山括弧の正体であるジェネリクスを、自分でクラスやメソッドを書く側の視点から解説していきます。

ジェネリクスとは?

型を後から指定できる仕組み

ジェネリクス(クラスやメソッドが扱うデータ型を、使う側で自由に指定できるようにする仕組み)は、<T>のような山括弧付きの型パラメータで表現します。
List<String>であれば「Stringを扱うList」、List<Integer>であれば「Integerを扱うList」というように、同じListクラスを様々な型で使い分けられるのはジェネリクスのおかげです。

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

ジェネリクスが導入される前のJavaでは、コレクションは何でも入るObject型として扱われていました。
その結果、実行時までミスに気づけないという問題がありました。

// ジェネリクスが無かった時代のイメージ(Object型のList)
List oldList = new ArrayList();
oldList.add("文字列");
oldList.add(123); // 数値も入ってしまう

String value = (String) oldList.get(1); // 実行時エラー(ClassCastException)

ジェネリクスを使うと、コンパイル時に型の不一致を検出できるようになり、実行してからエラーに気づくという事態を防げます。

基本の書き方

ジェネリッククラスを自作する

型パラメータを使うと、様々な型に対応できる汎用的なクラスを1つだけ書けば済みます。

public class Box<T> {
    private T content;

    public void set(T content) {
        this.content = content;
    }

    public T get() {
        return content;
    }
}
Box<String> stringBox = new Box<>();
stringBox.set("かつコーチ");
String name = stringBox.get(); // キャスト不要でそのままString型として使える

Box<Integer> intBox = new Box<>();
intBox.set(100);
int score = intBox.get();

Tは「Type」の頭文字で、慣例的に使われる型パラメータ名です(他にEはElement、KはKey、VはValueがよく使われます)。

ジェネリックメソッドを定義する

クラス全体ではなく、メソッド単位で型パラメータを持たせることもできます。

public class ArrayUtil {
    // メソッド自身が型パラメータ<T>を持つ
    public static <T> T getFirst(T[] array) {
        return array[0];
    }
}
String[] names = {"Aさん", "Bさん", "Cさん"};
String first = ArrayUtil.getFirst(names);
System.out.println(first); // Aさん

Integer[] numbers = {10, 20, 30};
int firstNum = ArrayUtil.getFirst(numbers);
System.out.println(firstNum); // 10

戻り値の型の前に<T>を宣言することで、呼び出し時の引数の型に応じてTが自動的に決まります。

複数の型パラメータを使う

Map<K, V>のように、型パラメータは複数持たせることもできます。

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
}
Pair<String, Integer> pair = new Pair<>("かつコーチ", 30);
System.out.println(pair.getKey() + ": " + pair.getValue()); // かつコーチ: 30

キーと値のペアを表現したい場合など、実務でもよく使われる形です。

よくあるつまずきポイント・エラー対処

プリミティブ型を型パラメータに直接指定してエラーになった

私が実際にハマった一次情報として、Box<int>のようにプリミティブ型をそのまま指定してコンパイルエラーになったことがあります。

// ❌Before:プリミティブ型を型パラメータにそのまま指定する
Box<int> intBox = new Box<>(); // コンパイルエラー

このコードをコンパイルすると、次のエラーが発生します。

error: unexpected type
Box<int> intBox = new Box<>();
    ^
  required: reference
  found:    int

ジェネリクスの型パラメータには、参照型(クラスやインターフェースなど、intdoubleのようなプリミティブ型ではない型)しか指定できません。
これはジェネリクスがコンパイル時に型情報を消去する「型消去(type erasure)」という仕組みで実現されており、内部的にはすべてObjectとして扱われるためです。

// ✅After:ラッパークラス(IntegerやDouble)を使う
Box<Integer> intBox = new Box<>();
intBox.set(100); // オートボクシングにより int → Integer に自動変換される
int value = intBox.get(); // オートアンボクシングにより Integer → int に自動変換される

intに対するIntegerdoubleに対するDoubleのように、プリミティブ型ごとに対応するラッパークラスが用意されているので、ジェネリクスを使う際はこちらを利用します。

応用・一歩先の使い方

ダイヤモンド演算子で右辺の型指定を省略する

Java 7以降では、右辺の<>部分の型指定を省略できる「ダイヤモンド演算子」が使えます。

// 従来の書き方(冗長)
Map<String, List<Integer>> scoreMap = new HashMap<String, List<Integer>>();

// ダイヤモンド演算子を使った書き方
Map<String, List<Integer>> scoreMap2 = new HashMap<>();

左辺の型情報からコンパイラが右辺の型を推論してくれるため、現在ではこちらの書き方が一般的です。

ワイルドカード(?)で柔軟に受け取る

「型は何でもいいので、とにかくListを受け取りたい」という場合は、ワイルドカード?を使います。

public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}
printAll(List.of("A", "B", "C"));
printAll(List.of(1, 2, 3));

より細かく「上限」「下限」を指定したい場合はextendssuperを使いますが、これは次回のジェネリクスの境界の記事で詳しく扱います。

まとめ

この記事のポイント

  • ジェネリクスは型を使う側で指定できる仕組みで、<T>のような型パラメータで表現する
  • クラス単位(ジェネリッククラス)・メソッド単位(ジェネリックメソッド)のどちらでも定義できる
  • Pair<K, V>のように複数の型パラメータを持たせることもできる
  • 型パラメータにはプリミティブ型ではなくIntegerなどのラッパークラスを使う
  • ダイヤモンド演算子<>で右辺の型指定を省略でき、ワイルドカード?で柔軟な受け取りができる

次に読むべき記事

  • ジェネリクスの境界(extends・super)
  • Listの基本:ArrayListとLinkedListの違い

タグ: Java, 中級者向け, コレクション

タイトルとURLをコピーしました