Java
2021.12.07
Javaのジェネリクス(総称型)とは?使い方をわかりやすく解説
2023.11.18


1.Javaのジェネリクス(Generics・総称型)とは?

ジェネリクスは、Javaをはじめとする多くのオブジェクト指向型言語で採用されている仕組みです。
オブジェクトの型の安全性と再利用性を兼ね備えた仕組みを提供します。


Javaでは、ジェネリクスは<>で囲んで利用します。もっとも多い利用例がリストでしょう。

List<String> caps = new ArrayList<>();

このとき、Listの宣言時に<〜>で囲むことで型を指定できる機能を「ジェネリクス」と呼びます。


また、ジェネリクスとして<〜>で囲んだ指定部分(この場合はString)を型パラメータと呼びます。


ArrayListはさまざまなクラスに対応した汎用的なクラスですが、型パラメータとしてStringを指定することで、Stringに特化したクラスとして振る舞うことが可能になるのです。



2. ジェネリクスを使うメリット


ここでは、ジェネリクスを使った場合のメリットを3つ紹介します。

①さまざまな型を入れられるものに対して型を限定できる

ジェネリクスは、共通の処理を提供していながら、利用するクラスの型を制限できます。


たとえばリストの場合、ジェネリクスとしてリストに格納できるクラスの型を指定します。Stringで型指定を行った場合、数値をリストに追加しようとするとコンパイルエラーが発生するのです。

List<String> arr = new ArrayList<>();
arr.add("Hello, World!");
arr.add(12); // -> コンパイルエラー

一方で、型指定としてInteger(整数)を指定した場合、文字列を追加しようとするとコンパイルエラーが発生します。
(intではなくIntegerを指定する理由については後述します。)

List<Integer> arr = new ArrayList<>();
arr.add(12);
arr.add("Hello, World!"); // -> コンパイルエラー

このように、ArrayListのクラス定義は単一でありながら、そのリストが扱うことのできるオブジェクトの型は制限されます。


こうすることで、想定外の型の値が代入された場合にはコンパイルエラーになり実行前に検知できるため、結果として予期せぬエラーの発生を抑えることが可能です。

②何の型を扱っているのかがわかりやすい

ジェネリクスを利用することで、どのクラスを用いているのかが一目瞭然になります。


ジェネリクスが登場する前のリストでは、リストに入れるべき型が指定できませんでした。そのため、リストの変数名や他のコードからリストに入れる型を推測する必要があったのです。


ジェネリクスが登場した後のリストでは次のようなコードを記載でき、charsは文字列のリスト、numbersは整数のリストを指定していることがわかります。

// 文字列のリスト
List<String> chars;
// 数値のリスト
List<Integer> numbers;

コーディングの時点で代入できる型を確認できるため、効率の良いコーディングが可能になるのです。

③データ型の不一致による実行時エラーを防げる

ジェネリクス登場前のリストは型を指定できなかったため、どんな型のインスタンスであっても同じリストに投入することが可能でした。


そのため、以下のようなコードもコンパイルエラーが発生せず実行可能だったのです。

List chars = new ArrayList();

chars.add("Hello ");
chars.add(3)
chars.add(" World");
chars.add('!');

また、この頃はリストから値を取り出す場合には、「キャスト」をする必要がありました。

String first = (String)chars.get(0);

リストに文字列が代入されていればエラーは発生しませんが、間違って数値を代入していた場合、実行時にエラーが発生してしまいます。


ジェネリクスが登場したことで、キャストも自動で行われます。これにより、安全に利用することが可能になりました。

List<String> chars = new ArrayList<String>();

// 〜代入処理は省略

// Stringの型として内容を取得できる
String first = chars.get(0);



3. ジェネリクスクラスの使い方


例として、ジェネリクスを活用したクラスを紹介します。今回は、ふたつの値を管理するような汎用クラスを作ってみましょう。


PairMap.java

public class PairMap<T> {

  private T first;
  private T second;

  public T first() {
    return this.first;
  }

  public T second() {
    return this.second;
  }

  public PairMap(T first, T second) {
    this.first = first;
    this.second = second;
  }
}


Main.java

public class Main {
  public static void main(String[] args) throws Exception {
    // 文字列のPairを作成
    PairMap pair = new PairMap<String>("ひとつめ", "ふたつめ");
    
    System.out.println("first is " + pair.first());
    System.out.println("Second is " + pair.second());
    
    System.out.println("----------");
    // 数字のPairを作成
    PairMap pair2 = new PairMap<Integer>(4, 2);
    System.out.println("first is " + pair2.first());
    System.out.println("Second is " + pair2.second());
  }
}

これを実行すると、次のように出力されます。

first is ひとつめ
Second is ふたつめ
----------
first is 4
Second is 2

それぞれ、文字列または数字を指定して正常に実行されていることが確認できます。


また、以下のようなコードを書いた場合にエラーとなることも確認しておきましょう。

PairMap pair3 = new PairMap<String>("ひとつめ", 2);

この場合、ジェネリクスで指定したStringと、2番目の引数の型が一致しないためコンパイルエラーとなります。



4.ジェネリクスメソッドの使い方

ここまではクラス全体としてジェネリクスを指定する方法を紹介しました。次は、メソッドの処理としてジェネリクスを指定する方法を紹介します。


次のMainクラスを作成して実行してみましょう。

import java.util.*;

public class Main {
  public static void main(String[] args) throws Exception {
    List<String> singleList = Main.yield("単文字");
    System.out.println(singleList);
  }

  /**
   * オブジェクトから対応したリストを生成するメソッド
   * @param T obj 対象オブジェクト
   * @returns 生成されたリスト
   */
  public static <T> List<T> yield(T obj) {
    // リストを生成
    List<T> list = new ArrayList<T>();
    // 生成したリストに引数のオブジェクトを指定
    list.add(obj);
    // 生成されたオブジェクトを返却
    return list;
  }
}

実行すると、次のように出力されます。

[単文字]

ここで、yieldメソッドは、引数として受け取ったオブジェクトのクラスに対応したリストを生成し返却します。


今回紹介したサンプルでは文字列を指定したため、List<String>が返却されました。独自のクラスを指定した場合には、そのクラスのリストが返却されます。



5.ジェネリクスのワイルドカードとは?


ジェネリクスを利用する際に、特定のクラスだけでなく、ある程度自由度をもった指定も可能です。


これらのテクニックは、システム全体で利用するような共通部品で活用できます。ジェネリクスの基本的な使い方と合わせてぜひ覚えてみてください。


ここでは、リストを例とした便利な使い方を紹介します。

①境界ワイルドカード型

ジェネリクスは便利な反面、すべてのクラスに対応できる汎用的なクラスを作るのが理想ですが、そうもいきません。

上限境界ワイルドカード型

上限境界ワイルドカードは、特定のクラスを継承、またはインターフェースを実装したクラスのみを引数として受け取るために使用します。


例として、次のコードは数値をジェネリクスとして指定したリストのみを引数に指定できるメソッドを紹介します。

public static void main(String[] args) throws Exception {
  List<Double> doubles = new ArrayList<>();
  doubles.add(4.2);
  doubles.add(8.2);
  System.out.println(getFirstWithInt(doubles));
}

/**
 * 最初の要素をintとして返却
 * @param 数値要素の配列
 * @return 最初の要素をintにキャストした値
 */
private static int getFirstWithInt(List<? extends Number> list) {
  Number num = list.get(0);
  return num.intValue();
}

これを実行すると、次のように出力されます。

4

なお、mainメソッドに以下のコードを追加した場合にはコンパイルエラーが発生します。

List<String> chars = new ArrayList<>();
chars.add("24");
System.out.println(getFirstWithInt(chars));

getFirstWithIntメソッドのジェネリクスに指定したNumberは、intのラッパークラスであるIntegerのスーパークラスです。


この場合、Numberを継承したDoubleをジェネリクスとして指定しているため正常に動作します。


一方で、StringクラスはNumberクラスを継承していないため、型の不一致としてコンパイルエラーとなります。

下限境界ワイルドカード型

ジェネリクスとして指定できる範囲として下限を指定することで、特定のクラスのスーパークラスを引数として受け取れるメソッドを作ることが可能です。


例として、数値リストの末尾に整数の値を追加するメソッドを紹介します。

public static void main(String[] args) throws Exception {
  List<Number> doubles = new ArrayList<>();
  doubles.add(4.2);
  doubles.add(8.2);

  putNumberToList(doubles, 24);
  System.out.println(doubles);
}

/**
 * 数値リストに整数値を追加する
 * @param list 追加対象のリスト
 * @return value 追加する値
 */
private static void putNumberToList(List<? super Integer> list, int value) {
  list.add(value);
}

これを実行すると、以下のように出力されます。

[4.2, 8.2, 24]

一方で、同じNumberを継承したFloatのリストを引数に指定した場合にはコンパイルエラーとなります。

List<Float> floats = new ArrayList<Float>();
putNumberToList(floats, 4); // -> コンパイルエラー!

このように、追加したいクラスの型を下限境界ワイルドカードとして指定することで、安全にリストへ値を追加できます。

上限と下限の使い分け

リストを例とした場合、下限と上限のワイルドカードは次のように使い分けると考えましょう。

・リストから値を取り出して使いたい場合には「上限境界」

・リストに値を追加したい場合には「下限境界」

②非境界ワイルドカード型

ジェネリクスの型パラメータとしてすべての型を利用可能としたい場合には、「非境界ワイルドカード」を使用します。


境界型ワイルドカードと異なり、<?>と指定することですべての型を受け付けるようになります。


また、非境界ワイルドカードを利用した場合、すべてのクラスのスーパークラスであるObjectを指定した場合と同等として扱われるので、覚えておきましょう。

/**
 * 最初の要素を文字として返却
 * @param list リスト
 * @return 最初の要素
 */
private static String getFirstWithString(List<?> list){
  return list.get(0).toString();
}

ただし、ジェネリクスの利点である「型制限」が使えないことになるため、よほどの理由がない限りは利用するべきではない、と認識しておきましょう。



6.覚えておくと便利なジェネリクスに関する知識


ジェネリクスを用いたクラスを設計する場合に、覚えておくとよい事項を紹介します。

①ジェネリクスの命名慣習

ジェネリクスを使用する場合、TやVといった英字を利用してジェネリクスの型を指定します。


それらの命名には以下の慣習がありますので、ジェネリクスを用いたクラスを作る場合には参考にしてみてください。

記号説明
E要素(Element)
Kキー(Key)
T型(Type)
V値(Value)
N数値(Number)
S, U, V2番目、3番目、4番目…

②ジェネリクスの複数の型のパラメータ

ジェネリクスには、複数のパラメータとしてジェネリクスの型を指定できます。


たとえば、HashMapクラスはキー(Key)と値 (Value)のふたつの型を指定できます。

Map<String, Integer> numMap = new HashMap<String, Integer>();
numMap.put("Key1", 12);

System.out.println(numMap.get("Key1"));




7.ジェネリクスを使う際の注意点

①ジェネリクスの型として int などの基本データ型は利用不可

ジェネリクスの型として指定できるのはクラスのみです。
そのため、intやfloatといった基本データ型(プリミティブ型)は指定できません。

List<int> intList = new ArrayList<int>();
// -> エラーになる

ラッパークラスを使う

もし基本データ型をジェネリクスの型として指定したい場合には、ラッパークラスを指定しましょう。

List<Integer> intList = new ArrayList<Integer>();

②ジェネリクスを用いたクラス中でパラメータ型の配列は作成できない

ジェネリクスを利用したクラスの中では、型パラメータとして指定したクラスの配列を作成することはできません。

public class ListAdapter<T> {

  private T[] tArray;

  public ListAdapter() {
    tArray = new T[];
    // -> コンパイルエラー!
  }
}


配列ではなくリストを使う

ジェネリクスを使った汎用クラスを作る場合には、リストを使うようにしましょう。

リストであれば値の追加も簡単に行うことが可能ですので、より柔軟性の高いクラスが作れます。

public class ListAdapter<T> {
  private List<T> tArray;

  public ListAdapter() {
    tArray = new ArrayList<T>();
  }

  public void addData(T obj) {
    this.tArray.add(obj);
  }
}



この記事をシェア