Javaジェネリックスの基本

1前書き

Java Genericsは、バグを減らし、型に対する抽象化の層を追加することを目的として、JDK 5.0で導入されました。

この記事は、Generics in Javaの概要、その背後にある目標、およびそれらを使用してコードの品質を向上させる方法について説明しています。

2ジェネリック医薬品の必要性

Integer を格納するためにJavaでリストを作成したいというシナリオを想像してみましょう。次のように書きたくなるでしょう。

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

驚いたことに、コンパイラは最後の行について文句を言うでしょう。どのデータ型が返されるのかわかりません。コンパイラは明示的なキャストが必要です。

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

リストの戻り値の型が Integerであることを保証できる規約はありません。定義済みリストには任意のオブジェクトを入れることができます。コンテキストを調べてリストを取得していることだけがわかります。型を見るとき、それが Object__であることを保証することしかできないので、型が安全であることを保証するために明示的なキャストが必要です。

このキャストはいらいらすることがあります、私たちはこのリストのデータ型が Integer であることを知っています。キャストはまた私達のコードを雑然とさせています。プログラマが明示的なキャストを間違えると、型関連の実行時エラーが発生する可能性があります。

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

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

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

型を含むひし形演算子<>を追加することによって、このリストの特殊化を Integer typeのみに絞り込みます。つまり、リスト内に保持される型を指定します。コンパイラはコンパイル時に型を強制できます。

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

3一般的な方法

ジェネリックメソッドは、単一のメソッド宣言で書かれたメソッドで、さまざまな型の引数で呼び出すことができます。どちらのタイプが使用されても、コンパイラーはその正確性を保証します。これらはジェネリックメソッドのいくつかのプロパティです。

  • 一般的なメソッドはtypeパラメータを持っています

メソッド宣言の戻り型の前の型) ** 型パラメータは制限することができます。

記事) ** ジェネリックメソッドはコンマで区切られた異なる型パラメータを持つことができます

メソッドシグネチャ内 ** ジェネリックメソッドのメソッド本体は通常のメソッドとまったく同じです。

配列をリストに変換するための一般的なメソッドを定義する例

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

前の例では、メソッドシグネチャの <T> は、メソッドがジェネリック型 T を扱うことを意味します。メソッドがvoidを返す場合でもこれは必要です。

前述のように、メソッドは複数のジェネリック型を扱うことができます。この場合、すべてのジェネリック型はメソッドシグネチャに追加する必要があります。 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. 型の要素を持つリストに変換する関数を渡します。例として、 Integer をその String 表現に変換することができます。

@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"));
}

Oracleの推奨事項では、ジェネリック型を表すために大文字を使用し、仮型を表すためにもっと説明的な文字を選択することをお勧めします。

3.1. 限定ジェネリックス

前述のように、型パラメータは制限することができます。制限されたとは「 制限された 」を意味します。メソッドで受け付けることができる型を制限することができます。

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

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

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

ここではキーワード extends を使用して、クラスの場合はタイプ T が上限を拡張するか、インターフェースの場合は上限を実装します。型は、次のように複数の上限を持つこともできます。

<T extends Number & Comparable>

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

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

ワイルドカードは、Javaの疑問符「 」で表され、未知の型を参照するために使用されます。ワイルドカードは、総称を使用するときに特に有用であり、パラメータータイプとして使用できますが、最初に考慮すべき重要な注意事項があります。

  • Object はすべてのJavaクラスのスーパータイプであることが知られていますが、 Object のコレクションはどのコレクションのスーパータイプでもありません。

たとえば、 List <Object> List <String> のスーパータイプではなく、 List <Object> 型の変数を List <String> 型の変数に代入すると、コンパイラエラーが発生します。これは、異種の型を同じコレクションに追加した場合に発生する可能性のある競合を防ぐためです。

同じ規則は、タイプとそのサブタイプのすべてのコレクションに適用されます。

この例を考えてください。

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

たとえば House のように Building のサブタイプを想像すると、 House Building のサブタイプであっても、 House のリストでこのメソッドを使用することはできません。このメソッドをBuildingタイプとそのすべてのサブタイプで使用する必要がある場合は、境界ワイルドカードを使用して魔法をかけることができます。

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

このメソッドは Building 型とそのすべてのサブタイプで動作します。

これは、 Building 型が上限である上限ワイルドカードと呼ばれます。

ワイルドカードは下限を指定して指定することもできます。未知のタイプは指定されたタイプのスーパータイプでなければなりません。下限は、 super キーワードとそれに続く特定のタイプを使用して指定できます(例: <?)。 super T> は、 T (= Tとそのすべての親)のスーパークラスである未知の型を意味します。

5消去タイプ

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

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

これは消去タイプの例です。

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

コンパイル時に、無制限型 T は、次のように Object に置き換えられます。

public List<Object> fromArrayToList(Object a) {
    return list.stream().collect(Collectors.toList());
}

型が制限されている場合、型はコンパイル時に次のように制限されます

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

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

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

6. 結論

Javaジェネリックスは、プログラマーの仕事を容易にし、エラーを起こしにくいので、Java言語に強力に追加されています。ジェネリックスは、コンパイル時に型の正確性を強制します。最も重要なことは、アプリケーションに余分なオーバーヘッドを発生させることなくジェネリックアルゴリズムを実装できるようにすることです。

この記事に付随するソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang-syntax[GitHubで利用可能]です。