Java/ジェネリクス

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

Javaにおけるジェネリクスは、Java 1.5から追加された。C++のテンプレートに「似た」概念で、ジェネリックプログラミングをサポートする。

概要[編集]

例えば、以下のクラスを考える:

class Box {
  Object element;
  Box(Object element) {
    this.element = element;
  }
}

そして以下のコードを考える。

class Main {
  public static void main(String[] args) {
    Box boxOfString = new Box("hoge");
    Box boxOfInteger = new Box(Integer.valueOf(42));
    unwrapBox(boxOfString);
    unwrapBox(boxOfInteger); // !!! ClassCastException
  }
  
  /**
  * Stringが格納されているBoxのelementを取り出し、標準出力に表示する。
  * @param box Boxのインスタンス
  */
  public static void unwrapBox(Box box) {
    System.out.println((String) box.element);
  }
}

このとき、6行目の呼び出しはunwrapBoxの呼び出し契約に違反している。なおかつ、IntegerStringと継承関係がないため、無条件にClassCastExceptionという例外が送出される[注 1]。さらに、boxOfStringboxOfIntegerが相互代入可能なことで、将来コード量が増えた時―あるいはコピーアンドペーストでコードを書いたときに取り違えるリスクがある。ここで、ジェネリクスを使用してBoxの定義、及びMainのコードを一部修正する:

class Box<T> {
  T element;
  Box(T element) {
    this.element = element;
  }
}
    Box<String> boxOfString = new Box("hoge");
    Box<Integer> boxOfInteger = new Box(Integer.valueOf(42));
    unwrapBox(boxOfString);
    // unwrapBox(boxOfInteger); // コンパイルエラー
  }
  
  /**
  * Stringが格納されているBoxのelementを取り出し、標準出力に表示する。
  * @param box Boxのインスタンス
  */
  public static void unwrapBox(Box<String> box) {
    System.out.println(box.element);
  }
}

不等号のペア(< … >)の中に型が追加された。これを型変数と呼び、Boxについては格納されている要素の型を表す。ジェネリクスを使用して、いくつかの利点を得た:

  • boxOfStringboxOfIntegerを取り違えなくなった。
  • unwrapBox(boxOfInteger)でコンパイルエラーが発生するようになった。
  • unwrapBoxでClassCastExceptionが送出される可能性がなくなった。

このように、ジェネリクスは型システムの範囲内にとどまりつつ、ある程度の柔軟さを追加する。ジェネリクスはList、Set、MapなどといったJava Collection Frameworkのメンバーを使用するときにほとんどと言っていいほど現れる。

raw型[編集]

ジェネリクス版Boxで、Box boxOfString = ...と記述することもできる。これは1.4以前との後方互換性のために用意された機能で、raw型と呼ばれることがある。ジェネリックプログラミングの利点を損なう上、将来バージョンでは禁止になる可能性がある[1]とされているため、新規に書くコードでは使う理由がない。

共変性・反変性[編集]

型変数が追加されると厄介なことになる。例えば:

  • Box<String>Box<Integer>の関係性は?
  • Box<Number>Box<Integer>の関係性は?

答えは「どちらも関係性がない」となる。Javaの型システムでは、それぞれ関係性がない別個の型とみなされる。これを非変という。しかし、これだけでは不便である。例えば、java.util.Listを使った以下のメソッドを考える[注 2]:

public static <E> void copyBox(Box<E> from, Box<E> to) {
  to.element = from.element;
}

これはfromの中身をtoに代入。当然同じ型では動作する。しかし、copyList(dogBox, animalBox)などとすると途端にうまくいかなくなる。これは合理的[注 3]なので、ぜひとも行いたいところだ。そこで、copyBoxを修正する:

public static <E> void copyBox(Box<? extends E> from, Box<? super E> to) {
  to.element = from.element;
}

これでうまく行くようになった。? extends Eというのは、戻り値の部分にのみ型変数が出現し、代わりに共変になることを表す。? super Eというのは、引数の部分にのみ型変数が出現し、代わりに反変になることを表す。

つまり、Boxについて言えばこうだ:

共変
DerivedBaseのサブタイプ→Box<Derived>Box<Base>のサブタイプ
反変
DerivedBaseのサブタイプ→Box<Base>Box<Derived>のサブタイプ
非変
共変でも反変でもない→Box<Base>Box<Derived>に関係がない

PECS原則[編集]

Here is a mnemonic to help you remember which wildcard type to use:
PECS stands for producer-extends, consumer-super.

In other words, if a parameterized type represents a T producer, use <? extends T>; if it represents a T consumer , use <? super T>


ここで、どのワイルドカード型を使用するかを覚えるためのニーモニックを紹介します。

PECS は producer-extends, consumer-super の頭文字をとったものです。

言換えれば、もしパラメーター化された型 T が、プロデューサーを表すのであれば <? extends T> を使い、T がコンシューマーを表すのであれば <? super T> を使います。

—Joshua Bloch, Effective Java 3rd Ed.(2018); ITEM 31: USE BOUNDED WILDCARDS TO INCREASE API FLEXIBILITY

修正後のcopyBoxを見ても反映されていることがわかる。

C++のテンプレート、C#とのジェネリクスとの違い[編集]

端的に言えば、実装方針の違いである。C++は、「テンプレートの実体化」と呼ばれるように、テンプレートに与えられた型の組み合わせだけ、(見えない) コードが生成される。他方、Javaのジェネリクスでは「型消去」と呼ばれる方式でコードを一つにまとめている。具体的には、ジェネリクスを含んだメソッドやクラスは、実行時には型変数が全て展開され、シグネチャに型変数を含まない。ただし、classファイルにはメタデータとして型変数の情報が残っている。C#は共通言語基盤にジェネリクスを取り扱う命令が追加され、型消去なしにジェネリクスを実現している[2]

制約[編集]

Javaプログラミング言語における「制約」と呼ばれるいくつかの制限について説明します。

Javaにおいて、型変数はコンパイル時には存在しますが、実行時には消滅してしまいます。そのため、型変数を使用する場合にはいくつかの制限があります。

まず、型変数を new 演算子でインスタンス化することはできません。この場合、コンパイラは自動的に型変数の上限型を選択しようとしますが、上限型が明示的に指定されていない場合はコンパイルエラーが発生します。

次に、型変数を配列の要素型として使用することもできません。これは、実行時に要素の型が必要なためですが、型変数は実行時には存在しないためです。

さらに、型変数を instanceof 演算子の被演算子として使用することはできません。これは、実行時にオブジェクトの型が必要なためで、型変数は実行時には存在しないためです。

また、型変数に対して class リテラルを呼び出すことはできません。これは、class リテラルが実行時にクラスの型を表すため、型変数が実行時には存在しないためです。

最後に、Throwable の派生クラスは型変数を持つことができません。これは、例外が発生した場合にスタックトレースが生成されるため、例外クラスの型が実行時に必要とされるためです。

class GenericThrowable<T> extends RuntimeException {}

class Main {
  public static void main(String[] args) {
    try {
      System.out.println("aaa");
      throw new GenericThrowable<String>();
    } catch (GenericThrowable<Integer> gti) {
      throw gti;
    } catch (GenericThrowable<String> gts) {
      System.out.println("GenericThrowable<Strng>");
    }
  }
}
GenericThrowable<T> という型変数を持つクラスが定義されています。
しかし、このクラスは Throwable の派生クラスであるため、上記の制限によりコンパイルできません。
また、 catch 節において、 catch ブロックに指定された型変数がコンパイル時には存在しなくなるため、どちらの catch 節を実行するか決定できなくなります。

注釈[編集]

  1. ^ この例外はキャストが成功しなかったことを示す。A型とされる式aからB型へのキャストが成功しなかったということは、aB型ではないということを意味する。
  2. ^ 本来であればaddAllメソッドを使うべきだが、ここではトピックを説明するためになかったものとする
  3. ^ 任意の動物が入るダンボール箱に犬を入れても、何も論理的破綻はないのを想像すると、自ずと理に適っていることが了解されるだろう

出典[編集]

  1. ^ Java言語仕様第3版§4.8
  2. ^ ジェネリック - C# によるプログラミング入門”. 2022年7月10日閲覧。

関連項目[編集]