「Java/ジェネリクス」の版間の差分

削除された内容 追加された内容
ページの作成:「Javaにおけるジェネリクスは、Java 1.5から追加された。C++のテンプレートに「似た」概念で、ジェネリックプログラミングをサ…」
(相違点なし)

2021年4月13日 (火) 22:02時点における版

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原則

PECSは、プロデューサー(producer)-extends, コンシューマー(consumer)-superを表しています。

―Effective Javaの項目28

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

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

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

制約

型変数をnewに使えない

上述で説明したとおり、型変数は実行時には展開されてしまう。そのため、newする型が自動的に型変数の上限型となり、意味がない。コード中で出現した場合、コンパイルエラーを生成する。

型変数を配列の要素型に使えない

同上。コード中で出現した場合、コンパイルエラーを生成する。

型変数をinstanceof演算子の被演算子として使えない

同上。コード中で出現した場合、コンパイルエラーを生成する。

型変数に対して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>");
    }
  }
}

このコードはコンパイルできない。イレイジャにより型変数が「消える」のでコンパイラはどちらのcatch節へ行くべきか決定不能でもある。

注釈

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

出典

  1. ^ Java言語仕様第3版§4.8
  2. ^ https://ufcpp.net/study/csharp/sp2_generics.html#compare 20210414

関連項目