The King's Museum

ソフトウェアエンジニアのブログ。

【Java・ジェネリックス】ワイルドカード型とは何か【前半】

Javaジェネリックスにあるワイルドカードについて解説するシリーズの前半。

ワイルドカード

ワイルドカード型とは型パラメータに ? の記号が書かれているジェネリックス型のことを指します。

ワイルドカード型には「非境界ワイルドカード型」と「境界ワイルドカード型」があり、以下のように分類されます。

非境界ワイルドカード

まず、非境界ワイルドカード型の方について説明します。 非境界ワイルドカード型は、型パラメータが ? だけのジェネリックス型です。 例えば、List<?> は List の非境界ワイルドカード型となります。

非境界ワイルドカード型は通常のパラメータ化されたジェネリックス型と何が違うのでしょうか。 端的に言ってしまえば、非境界ワイルドカード型はそのジェネリックス型のパラメータ化された型のスーパータイプとなる型です。

具体的に言うと、List<?> は非境界ワイルドカード型です。 List<String>List<Object>List<Number> は通常のジェネリックス型です。 このとき、List<?>List<String>List<Object>List<Number> のスーパータイプとなるのです。

Java では型パラメータ同士に継承関係があっても、ジェネリックス型自体には継承関係はありません。

f:id:hjm333:20160402153234p:plain

このジェネリックス型の性質を不変といいます。詳細は項目25を参照してください。 この性質のため、通常、ジェネリックスでは同じ型パラメータを持つ変数同士でしか代入できません。

ArrayList<Number> numbers = new ArrayList<Number>();
// => 問題無し

numbers = new ArrayList<Object>();
// => コンパイルエラー
// Number は Object のサブタイプだが、ArrayList<Number> は ArrayList<Object> のスーパータイプではない(不変)ので代入不可能

numbers = new ArrayList<Integer>();
// => コンパイルエラー
// Number は Integer のスーパータイプだが、ArrayList<Number> は ArrayList<Integer> のスーパータイプではない(不変)ので代入不可能

しかし、非境界ワイルドカード型はすべてのパラメータ化された型のスーパータイプとなります。

f:id:hjm333:20160402153400p:plain

そのため、どんな List も、List<?> に代入できます。

ArrayList<?> list = new ArrayList<Number>();
// => 問題無し

list = new ArrayList<Object>();
// => 問題無し

list = new ArrayList<Integer>();
// => 問題無し

非境界ワイルドカードの利用方法

非境界ワイルドカード型は「ジェネリックスを利用したいけど、型パラメータに何が含まれるか分からない」という場合に利用します。 例えば「任意の型でパラメータ化された List のすべての要素を出力する関数を作りたい」というような場合に有用です。

実際にこのようなメソッドを以下のようにして書くことができます。

static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

この printList の引数では非境界ワイルドカードを使っています。 そのため、どのような型パラメータを持つ List も引数として受け付けることができます。

List<String> strings = new ArrayList<String>();
List<Integer> integers = new ArrayList<Integer>();

printList(strings);
// => 問題なし
printList(integers);
// => 問題なし

非境界ワイルドカード型の制限

非境界ワイルドカード型は、任意のパラメータ化された List のスーパータイプとなり便利に利用できる一方、以下のような制限があります。

  • メソッドの戻り値に使われている T は Object 型になる
  • メソッドの引数に使われている T には null リテラルしか渡せない

この制限を具体的に見ていきます。 例として任意の型 T を持つ値を持つだけの Holder ジェネリッククラスを使います。

static class Holder<T> {
    private T value;

    public Holder(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }   
}

メソッドの戻り値の T は Object 型

この Holder の非境界ワイルドカードを利用して、getValue() で this.value の値を取得してみます。

Holder<?> holder = new Holder<String>("Value");

String string = holder.getValue();
// => コンパイルエラー
// 非境界ワイルドカード型は型パラメータが不明なため、型パラメータを String だと保障できない

Integer integer = holder.getValue();
// => コンパイルエラー
// 非境界ワイルドカード型は型パラメータが不明なため、型パラメータを Integer だと保障できない

Object object = holder.getValue();
// => 問題なし
// 非境界ワイルドカード型は型パラメータが不明なため何が入っているか分からないが、すべての参照型のスーパータイプである Object なら絶対に ClassCastException は発生しないため

この例が示すように、非境界ワイルドカード型のメソッドの戻り値として許されるのは Object のみです。

コンパイラは、以下のような理屈に基づいて、型安全性を保障するためにこの制限を用いています。

何が入っているかは分からないが、すべての参照型は Object のサブタイプである。 たとえどんな型か分からなくても、Object 型の変数なら ClassCastException を発生させずに代入可能。

メソッドの引数の T は null リテラル

今度は、setValue() を使って this.value に値を代入してみます。

Holder<?> holder = new Holder<String>();

holder.setValue(new String("String"));
// => コンパイルエラー
// 非境界ワイルドカード型は型パラメータが不明なため、String を this.value に代入可能だと保障できない
// たとえば、this.value は Integer 型かもしれない。

holder.setValue(new Object());
// => コンパイルエラー
// 非境界ワイルドカード型は型パラメータが不明なため、Object を this.value に代入可能だと保障できない
// たとえば、this.value は Integer 型かもしれない。

holder.setValue(null);
// => 問題なし
// 非境界ワイルドカード型は型パラメータが不明なため this.value の型が何か分からないが、null はどんな変数にも代入できるので ClassCastException は発生しない

この例が示すように、非境界ワイルドカード型のメソッドの引数に使われている T 型には null リテラルしか代入できません。

コンパイラは、以下のような理屈に基づいて、型安全性を保障するためにこの制限を用いています。

代入先の型が何かは分からないが、null リテラル値はどんな変数にも代入できる。 たとえどの型か分からなくても、null リテラル値なら ClassCastException は発生しえない。

null は Java の仕様上、null 型という特殊な型の唯一の値であり、どんな参照型の変数に代入できることが定められています。*1

まとめ

今回はワイルドカード型の中でも特に非境界ワイルドカード型について説明しました。

  • ワイルドカードには「非境界型」と「境界型」がある
  • 非境界ワイルドカード型は、そのジェネリックスのパラメータ化されたすべての型のスーパータイプとなる
  • 非境界ワイルドカード型には型安全を保障するための以下の制限がある。
    • メソッドの戻り値に使われている T は Object 型になる
    • メソッドの引数に使われている T には null リテラルしか渡せない

後半では、ワイルドカード型の性質や PECS との関係について説明します。

hjm333.hatenablog.com

感想

ワイルドカードのことはよく分かってなかったからちゃんとまとめようとして、いろいろ調べた結果、ぜんぜんまとまらなくなってしまったので二回に分けることにした。

Effective Java の次章をはやく進めたい気持ちもあるのだけど、せっかくだからちゃんと理解しようと思ってそれなりに時間をかけて勉強している。 その甲斐あってだいぶ理解が深まって、頭の中の霧が晴れてきた感じがする。

(c) The King's Museum