1. 概要

JDK 5.0は、バグを減らし、型に抽象化レイヤーを追加することを目的として、JavaGenericsを導入しました。

このチュートリアルは、JavaのGenericsの簡単な紹介であり、その背後にある目標と、それらがコードの品質をどのように改善できるかを示しています。

2. ジェネリックの必要性

Javaにリストを作成して整数を格納するシナリオを想像してみましょう。

私たちは次のように書こうとするかもしれません:

List list = new LinkedList();
list.add(new Integer(1)); 
Integer i = list.iterator().next();

驚いたことに、コンパイラは最後の行について文句を言います。 どのデータ型が返されるかはわかりません。

コンパイラーは明示的なキャストを必要とします:

Integer i = (Integer) list.iterator.next();

リストの戻りタイプが整数であることを保証できるコントラクトはありません。 定義されたリストは、任意のオブジェクトを保持できます。 コンテキストを調べてリストを取得していることだけがわかります。 タイプを見るとき、それが Object であることを保証することしかできないため、タイプが安全であることを保証するために明示的なキャストが必要です。

このキャストは煩わしい場合があります—このリストのデータ型は整数であることがわかっています。 キャストもコードを乱雑にしています。 プログラマーが明示的なキャストを間違えると、タイプ関連のランタイムエラーが発生する可能性があります。

プログラマーが特定の型を使用する意図を表現でき、コンパイラーがそのような型の正確さを保証できれば、はるかに簡単になります。 これがジェネリックの背後にあるコアアイデアです。

前のコードスニペットの最初の行を変更してみましょう。

List<Integer> list = new LinkedList<>();

タイプを含むダイヤモンド演算子<>を追加することにより、このリストの専門分野をのみに絞り込みます。 整数タイプ。 つまり、リスト内に保持されているタイプを指定します。 コンパイラーは、コンパイル時に型を強制できます。

小さなプログラムでは、これは些細な追加のように見えるかもしれません。 ただし、大規模なプログラムでは、これにより大幅な堅牢性が追加され、プログラムが読みやすくなります。

3. 一般的な方法

単一のメソッド宣言を使用して汎用メソッドを記述し、さまざまな型の引数を使用してそれらを呼び出すことができます。 コンパイラーは、使用するタイプの正確さを保証します。

一般的なメソッドのいくつかのプロパティは次のとおりです。

  • 汎用メソッドには、メソッド宣言の戻り型の前に型パラメーター(型を囲むひし形演算子)があります。
  • タイプパラメータは制限できます(範囲についてはこの記事の後半で説明します)。
  • ジェネリックメソッドは、メソッドシグネチャでコンマで区切られたさまざまなタイプパラメータを持つことができます。
  • 一般的なメソッドのメソッド本体は、通常のメソッドとまったく同じです。

配列をリストに変換するジェネリックメソッドを定義する例を次に示します。

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

The メソッドシグニチャ内は、メソッドがジェネリック型を処理することを意味します T 。 これは、メソッドがvoidを返している場合でも必要です。

前述のように、このメソッドは複数の汎用タイプを処理できます。 この場合、すべてのジェネリック型をメソッドシグネチャに追加する必要があります。

タイプTおよびタイプGを処理するために、上記のメソッドを変更する方法は次のとおりです。

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

T型の要素を持つ配列をG型の要素を持つリストに変換する関数を渡します。

例として、IntegerString表現に変換します。

@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
    Integer[] intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);
 
    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

オラクルの推奨事項は、一般的なタイプを表すために大文字を使用し、正式なタイプを表すためにより説明的な文字を選択することです。 Javaコレクションでは、タイプに T 、キーに K 、値にVを使用します。

3.1. バウンドジェネリックス

タイプパラメータは制限できることに注意してください。 Boundedは「制限付き」を意味し、メソッドが受け入れるタイプを制限できます。

たとえば、メソッドが型とそのすべてのサブクラス(上限)または型とそのすべてのスーパークラス(下限)を受け入れるように指定できます。

上限の型を宣言するには、型の後にキーワード extends を使用し、その後に使用する上限を使用します。

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

ここでは、キーワード extends を使用して、タイプ T がクラスの場合は上限を拡張し、インターフェイスの場合は上限を実装することを意味します。

3.2. 複数の境界

タイプには複数の上限を設定することもできます。

<T extends Number & Comparable>

T によって拡張されるタイプの1つがクラスである場合(例: Number )、境界のリストの最初に配置する必要があります。 そうしないと、コンパイル時エラーが発生します。

4. ジェネリックスでのワイルドカードの使用

ワイルドカードは、Javaでは疑問符で表され、不明なタイプを参照するために使用されます。 ワイルドカードはジェネリックスで特に役立ち、パラメータータイプとして使用できます。

しかし、最初に、考慮すべき重要な注意事項があります。 ObjectはすべてのJavaクラスのスーパータイプであることがわかっています。ただし、Objectのコレクションはどのコレクションのスーパータイプでもありません。

たとえば、 リストのスーパータイプではありませんリスト 、およびタイプの変数の割り当てリストタイプの変数にリストコンパイラエラーが発生します。 これは、同じコレクションに異種タイプを追加した場合に発生する可能性のある競合を防ぐためです。

同じルールが、タイプとそのサブタイプのコレクションに適用されます。

この例を考えてみましょう。

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

HouseなどのBuilding のサブタイプを想像すると、 Houseであっても、Houseのリストでこのメソッドを使用することはできません。 は、Buildingのサブタイプです。

タイプBuildingとそのすべてのサブタイプでこのメソッドを使用する必要がある場合、制限付きワイルドカードは魔法を実行できます。

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

これで、このメソッドはタイプBuildingとそのすべてのサブタイプで機能します。 これは上限ワイルドカードと呼ばれ、タイプBuildingが上限です。

下限のワイルドカードを指定することもできます。この場合、不明なタイプは指定されたタイプのスーパータイプである必要があります。 下限は、superキーワードの後に特定のタイプを続けて指定できます。 例えば、 <? スーパーT> のスーパークラスである未知のタイプを意味します T (= Tとそのすべての親)。

5. 型消去

型の安全性を確保するために、ジェネリックスがJavaに追加されました。 また、ジェネリックが実行時にオーバーヘッドを引き起こさないようにするために、コンパイラはコンパイル時に型消去と呼ばれるプロセスをジェネリックに適用します。

型消去はすべての型パラメーターを削除し、それらを境界に置き換えるか、型パラメーターが制限されていない場合はObjectに置き換えます。 このように、コンパイル後のバイトコードには通常のクラス、インターフェイス、およびメソッドのみが含まれ、新しいタイプが生成されないようにします。 コンパイル時にObjectタイプにも適切なキャストが適用されます。

これは型消去の例です。

public <T> List<T> genericMethod(List<T> list) {
    return list.stream().collect(Collectors.toList());
}

型消去では、無制限の型TObjectに置き換えられます。

// for illustration
public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}

// which in practice results in
public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

タイプがバインドされている場合、そのタイプはコンパイル時にバインドされたものに置き換えられます。

public <T extends Building> void genericMethod(T t) {
    ...
}

コンパイル後に変更されます:

public void genericMethod(Building t) {
    ...
}

6. ジェネリックスとプリミティブデータ型

Javaのジェネリックの制限の1つは、typeパラメーターをプリミティブ型にすることはできないということです。

たとえば、以下はコンパイルされません。

List<int> list = new ArrayList<>();
list.add(17);

プリミティブデータ型が機能しない理由を理解するために、ジェネリックはコンパイル時の機能であることに注意してください。つまり、型パラメーターは消去され、すべてのジェネリック型は型Objectとして実装されます。

リストのaddメソッドを見てみましょう。

List<Integer> list = new ArrayList<>();
list.add(17);

addメソッドのシグネチャは次のとおりです。

boolean add(E e);

コンパイルされます:

boolean add(Object e);

したがって、タイプパラメータはObjectに変換可能である必要があります。 プリミティブ型はObjectを拡張しないため、型パラメーターとして使用することはできません。

ただし、Javaは、プリミティブのボックス化されたタイプと、それらをアンラップするための自動ボックス化およびアンボックス化を提供します

Integer a = 17;
int b = a;

したがって、整数を保持できるリストを作成する場合は、次のラッパーを使用できます。

List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);

コンパイルされたコードは、次のものと同等になります。

List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();

Javaの将来のバージョンでは、ジェネリックのプリミティブデータ型が許可される可能性があります。プロジェクト Valhalla は、ジェネリックの処理方法を改善することを目的としています。 アイデアは、 JEP218で説明されているジェネリックスペシャライゼーションを実装することです。

7. 結論

Java Genericsは、Java言語への強力な追加機能です。これにより、プログラマーの作業が簡単になり、エラーが発生しにくくなります。 ジェネリックスは、コンパイル時に型の正確性を強制し、最も重要なこととして、アプリケーションに余分なオーバーヘッドを発生させることなく、ジェネリックアルゴリズムの実装を可能にします。

記事に付属するソースコードは、GitHubから入手できます。