やり直しJava

総称型

Jump to Section

オブジェクト指向プログラミング」の章でも触れましたが、総称型はポリモーフィズムのパラメータ多相を実現するための仕組みで J2SE 5.0で追加されました。特定の型を指定せずにコードを記述し さまざまな型を適用できるようにしておき 実際に使用する際に型を指定することにより、コンパイル時に型安全性を担保することができます。総称型の「型」にはプリミティブ型を指定することはできず、「型」として指定できるのは クラス・インタフェース・列挙型・アノテーションになります。

始めに どのようなところで総称型が使われているかを見てから、標準ライブラリで用意されているような総称型のクラスやメソッドを利用する方法を見てみます。続いて総称型の仕組みなどの詳細を見てから 最後に自分で総称型のクラスやメソッドを定義する方法を見ていきます。

総称型として定義されているクラス・インタフェース

さまざまな型を汎用的に扱うクラスやインタフェースが 総称型として定義されています。具体的には次のようなクラスやインタフェースが総称型として定義されています。

  • コレクション
  • Optional
  • ストリーム
  • 関数型インタフェース
  • Classクラス

コレクション

コレクションのクラスやインタフェースは あらゆる型の要素を格納して汎用的に扱うため総称型として定義されています。

J2SE 1.4まではListやMap等のコレクションの要素はObject型でした。何でも格納できる変わりに 取り出すときにキャストが必要で コードが煩雑になりがちでした。また、間違ったクラスの要素を格納したり 取り出した要素を間違ったクラスにキャストしても コンパイラによるチェックが行えず 型の安全性を担保できませんでした。

J2SE 5.0から総称型が取り入れられ コレクションの要素の型指定ができるようになり これらの問題が解消されるようになりました。

Optional

Optionalは高々1つの要素を持てるコレクションのような物で、コレクション同様 あらゆる型の要素を格納して汎用的に扱うため総称型として定義されています。

ストリーム

ストリームのクラスやインタフェースは コレクションや配列のような集合的な要素に対する操作を行います。あらゆる型の要素を汎用的に取り扱うため 総称型として定義されています。

関数型インタフェース

関数型インタフェースは 関数の型(引数や戻り値)の定義を表すために用いられます。関数では様々な型を汎用的に扱うため 関数型インタフェースは総称型で定義されています。

Classクラス

java.lang.Classは クラス自体を表すクラスで、Class<T>と総称型として定義されています。例えばStringインスタンスのクラス(String.class)はClass<String>であり、Integerインスタンスのクラス(Integer.class)はClass<Integer>になります。様々な型を汎用的に扱うので総称型として定義されています。

また、Classのメソッドには 戻り値がT型に限定されるものや 継承関係が反映されるものがいくつかあり、総称型にすることによって戻り値の型が限定されることを表現しているものがあります。T型に限定されるものの例はcast()メソッドで 戻り値の型はTです。これは戻り値がT型に限定されることを表現しています。継承関係が反映されるものの例はgetSuperclass()メソッドで 戻り値の型はClass<? super T>です。これは戻り値の型がTのスーパークラスのClassに限定されることを表現しています。こういった表現は総称型が導入される前はできませんでした。

総称型クラスやメソッドの基本的な利用方法

始めに標準ライブラリの総称型で定義されているクラスの 利用の仕方から見て行き、続いて 総称型メソッドの利用の仕方、境界ワイルドカード型になっている場合の利用の仕方を見ていきます。

総称型の例としてjava.util.Listクラスを見てみると クラス定義が List<E>となっています。このEが型引数で 実際にListに格納する要素の型(クラス・インタフェースなど)を指定します。型引数はEだけではなく 慣例的に「T」(Type)、「E」(Element)、「K」(Key)、「V」(Value)などの先頭一文字が良く使われます

総称型クラスの基本的な利用例

総称型クラスの例としてjava.util.List<E>を挙げます。ここでは Listに格納する要素としてStringを指定しています。

List<String> strList = new ArrayList<>();  // Java SE7より以前は new ArrayList<String>();
strList.add("One");
strList.add("Two");
strList.add("Three");
String str = list.get(0);

ポイントとしては次の2点です。

  • 変数strListの宣言で型を”List<String>”としてListの要素の型をStringに特定している。
  • Listのget()メソッドで要素を取り出すときに、キャストすることなくStringとして取り出せる。

List<String>のようにList<E>の型引数に具体的な型(クラスやインタフェースなど)を指定した型をパラメータ化された型(parameterized type)と呼びます。

Java SE7より以前では1行目の右辺を “new ArrayList<String>();” とする必要がありましたが、Java SE7からコンパイラが型推論を行うため型指定を省略できるようになりました。(<>をダイヤモンド演算子と呼びます。)

総称型メソッドの利用例

クラス全体を総称型としなくても、メソッドのみを総称型とすることもできます。CollectionsやArraysといったユーティリティ的なクラスメソッドを提供するクラスは、クラス自体は総称型にはなっておらず メソッドのみが総称型になっています。総称型メソッドの例としてArrays.asList()を挙げます。メソッド定義は次のようになっています。

static <T> List<T> asList(T… a)

asList()は引数で渡した値を要素とする不変なリストを返します。

List<String> list1 = Arrays.asList("One", "Two", "Three");          // 右辺で型の指定は不要
List<String> list2 = Arrays.<String>asList("One", "Two", "Three");  // 右辺で型を指定してもOK

1行目の例のように右辺で型引数を指定する必要はありません。2行目の例のように右辺で型引数を指定することもできます。その場合はメソッド名の前に型引数を指定します。戻り値と引数の型が一致しない場合や 戻り値と明示した型引数が一致しない場合はコンパイルエラーになります。

// List<Integer> list3 = Arrays.asList("One", "Two", "Three");  // 戻り値と引数の型が不一致
// List<Object> list4 = Arrays.<String>asList("One", "Two", "Three");  // 戻り値と右辺の型引数が不一致

ワイルドカード型の利用例

ListクラスのAPIドキュメントを眺めると<?>、<? extends E>、<? super E>などが登場します。それぞれ非境界ワイルドカード型(<?>)、上限境界ワイルドカード型(<? extends E>)、下限境界ワイルドカード型(<? super T>)と言い、ここではまとめてワイルドカード型と呼びます。

ワイルドカード型の必要性を簡単に説明すると次のようになります。

JavaではIntegerはNumberのサブクラスなので Number型の変数にIntegerのインスタンスを格納することができますが、List<Number>型の変数にList<Integer>のインスタンスを格納することはできません。(後述しますが この関係を非変と言います)これは型安全性を保証する上では重要なのですが、直感的でなく柔軟性に欠けます。これに対して、型安全を保証しながら List<Number>型の変数にList<Integer>のインスタンスを格納するための仕組みが上限境界ワイルドカード型です。(逆にList<Integer>型の変数にList<Number>のインスタンスを格納できるようにするための仕組みが下限境界ワイルドカード型です。)

APIドキュメントを眺めると分かると思いますが、ワイルドカード型は ほとんどがクラスの型引数かメソッドの引数に登場し、メソッドの戻り値に登場することは稀です。(例外的に Classクラスでは 戻り値にもワイルドカード型が登場します。)

ワイルドカード型については 詳しくは後の章で説明しますが、まずはメソッドの引数がワイルドカード型の場合の利用例を見て行きます。

メソッド引数が非境界ワイルドカード型の場合の利用例

非境界ワイルドカード型を引数とするメソッドには List.containsAll()等があります。メソッド定義は次のようになっています。

public void containsAll(Collection<?> c)

containsAll()は 受け取ったコレクションの全ての要素が自身に含まれるかどうか判定します。引数のコレクションの要素の型が自身の要素の型と一致しなくても構わず、引数にはどんな要素型のコレクションも渡すことができるようになっています。

List<Number> list = new ArrayList<>();

List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();

list.containsAll(intList);    // サブクラスの要素のListもOK
list.containsAll(numList);    // 同じクラスの要素のListなので 勿論OK
list.containsAll(objList);    // スーパークラスの要素のListもOK
list.containsAll(strList);    // 継承関係がない要素のListもOKだが「unlikely-arg-type」の警告が出る

上の例のように、どのような要素の型のコレクションも引数として渡すことができますが、継承関係のない要素の型を指定するとコンパイラが警告を出します。

メソッド引数が上限境界ワイルドカード型の場合の利用例

上限境界ワイルドカード型を引数とするメソッドには List.addAll()等があります。メソッド定義は次のようになっています。

public boolean addAll(Collection<? extends E> c)

Listには1個の要素を追加するadd(E e)メソッドがありますが、add()メソッドではEのサブクラスも受け取ることができます。addAll()は受け取ったコレクションの要素全てを自身に追加しますが、同じ型のコレクションだけでなく サブクラスのコレクションも受け取れるようにして、add()メソッドと同等の使い勝手を提供しています。

List<Number> list = new ArrayList<>();

List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
List<String> strList = new ArrayList<>();

list.addAll(intList);     // サブクラスの要素のListもOK
list.addAll(numList);     // 同じクラスの要素のListなので 勿論OK
// list.addAll(objList);   // スーパークラスの要素のListはNG
// list.addAll(strList);   // 継承関係がないクラスの要素のListもNG

上の例のように、Numberかそのサブクラスのコレクションを引数として渡すことができます。

メソッド引数が下限境界ワイルドカード型の場合の利用例

下限境界ワイルドカード型を引数とするメソッドには List.sort()等があります。メソッド定義は次のようになっています。

default void sort(Comparator<? super E> c)

sort()は 渡されたComparator cに従って 自身の要素を並べ替えます。Comparator cはcompare()メソッドを実装する必要があり 2つの要素の比較処理を行います。2つの要素の比較処理は汎用的に行える場合が多いため、抽象度の高いクラスを扱うようにしておけば それぞれの派生クラスのComparatorを用意する必要がなくなります。

List<Number> list = new ArrayList<>();

Comparator<Object> objComp = (o1, o2) -> Integer.compare(o1.hashCode(), o2.hashCode());  // ハッシュコードで比較
Comparator<Number> numComp = (o1, o2) -> Double.compare(o1.doubleValue(),o2.doubleValue());    // double値で比較
Comparator<Integer> intComp = (o1, o2) -> Integer.compare(o1, o2);
Comparator<String> strComp = (o1, o2) -> o1.compareTo(o2);

list.sort(objComp);     // スーパークラスのComparatorはOK
list.sort(numComp);     // 同じクラスのComparatorなので 勿論OK
// list.sort(intComp);   // サブクラスのComparatorはNG
// list.sort(strComp);   // 継承関係がないクラスのComparatorはNG

上の例のように、NumberかそのスーパークラスのComparatorを引数として渡すことができます。

総称型の詳細

Javaにおける総称型の仕組みの概要

次のような単純な総称型クラスを例に挙げます。一つの要素を入れるための入れ物クラスです。

class Box<T> {
    T t;
    public T get() { return t;}
    public void put(T t) { this.t = t; }
}

利用例は次の通りです。

Box<String> strBox = new Box<>();
strBox.put("String");       // String以外を渡すとコンパイルエラー
String str = strBox.get();  // キャスト不要

Box<Double> doubleBox = new Box<>();
doubleBox.put(3.14);         // doubleとDouble以外を渡すとコンパイルエラー
Double d = doubleBox.get();  // キャスト不要

上のBoxクラスは、コンパイル時にコンパイラによって次のように置き換えられます。

class Box {
    Object t;
    public Object get() { return t;}
    public void put(Object t) { this.t = t; }
}

(※ この置き換えではBoxクラスが総称型でなくなってしまっていますが、生成したclassファイルにはBoxクラスが総称型であるという情報は保持されます。)

Box<String>やBox<Double>クラスが個別に生成されるのではなく、型引数TがObjectに置き換えられた Boxクラスのみが生成されます。(繰り返しになりますが、生成されたclassファイルにはBoxクラスが総称型であるという情報は保持されます)この型引数を伴わないBoxクラスを原型(raw type)と呼びます。

同様に、利用側のコードもコンパイル時にコンパイラによって次のように置き換えらます。

Box strBox = new Box();
strBox.put("String");                // String以外を渡すとコンパイルエラー
String str = (String) strBox.get();  // キャストを挿入

Box doubleBox = new Box();
doubleBox.put(3.14);         // double・Double以外を渡すとコンパイルエラー
Double d = (Double) doubleBox.get();  // キャストを挿入

strBoxもdoubleBoxもBoxクラスの原型をインスタンス化しています。また、型引数Tが戻ってくる部分にキャストを挿入して整合を取っています。strBoxもdoubleBoxも同じBoxになってしまい、実行時にはTがStringだったのかDoubleだったのか知ることができません(リフレクションでも取得できません)。

プログラミング言語によって総称型の実装方法は様々ですが、Javaのこの方式は イレイジャ(後述)によって実装していることからイレイジャ方式と呼ばれます。(C++の場合は 実際の型引数の分だけコードの複製が作成されるテンプレート方式を採用しています。)

Erasure of Generic Types」(Oracle:The Java Tutorials)

イレイジャ

イレイジャは J2SE 5.0から追加されたパラメータ化された型(List<String>など)や型引数を含む型(List<E>、Tなど)を、コンパイラによって J2SE 5.0より前から存在しているパラメータ化された型や型引数を含まない型に変換された物です。Java言語仕様の中で変換ルールが定義されています。「イレイジャ」は場合によっては変換自体を指していることもあります(言語仕様の中でも混在しているようです)。ここでは 便宜上 イレイジャへの変換を指す場合は イレイジャ変換と呼ぶことにします。次に具体的なイレイジャの例を挙げます。

イレイジャの具体例
型の分類具体例
パラメータ化された型List<String>のイレイジャはList
配列型String[]のイレイジャはString[]、List<String>[]のイレイジャはList[]
型変数TのイレイジャはObject、T extends NumberのイレイジャはNumber
(※ 継承ツリーの最上位の限定型)
その他の型StringのイレイジャはString、ListのイレイジャはList

コンパイル時にパラメータ化された型や型引数を除去してイレイジャにすることにより、J2SE 5.0より前のコードとのバイナリ互換を保っています

この章の「Javaにおける総称型の仕組みの概要」の中で Box<T>クラスのTがObjectに置き換えられたのは、イレイジャ変換のルールに従っています。また、利用例の中でBox<String>やBox<Double>がBoxに置き換えられたのも同様です。

パラメータ化された型や型引数は そのままでは実体化されず、イレイジャとして実体化されます。このような型を具象化不可能型(non-reifiable type)と呼びます。

メソッドシグニチャ

イレイジャはメソッドシグニチャを定義するためにも使われます。メソッドシグニチャはメソッドの名前と引数の型のリストでメソッドを特定します。オーバーロードやオーバーライドと関係が深く、メソッドシグニチャが同じメソッドを定義してしまうと二重定義となりオーバーロードすることができません。また、継承したクラスのメソッドをオーバーライドしたつもりが、メソッドシグニチャを間違えると別のメソッドを定義することになってしてしまいます。

J2SE 5.0で型引数や型変数が導入されたため、「引数の型」の定義が見直され 引数の型はイレイジャを指すことになりました。したがって、メソッドの名前と引数のイレイジャのリストが同じメソッドは 重複定義となりオーバーロードすることができません。総称型とメソッドシグニチャの重複の例を次に示します。

class Sample<T, U> {
    public void method1(List l) {};           // ①
//  public void method1(List<Integer> l) {};  // ①と重複するため定義できない
//  public void method1(List<String> l) {};   // 同様
//  public void method1(List<?> l) {};        // 同様

    public void method2(T arg) {};       // ② TのイレイジャはObject
//  public void method2(U arg) {};       // ②と重複。UのイレイジャもObject
//  public void method2(Object arg) {};  // ②と重複
    public void method2(Integer arg) {}; // イレイジャが違うのでOK

    public void method3(Number n) {};                // ③
//  public <V extends Number> void method3(V v) {};  // ③と重複。VのイレイジャはNumber
}

原型

総称型には 型引数を指定しない原型(raw type)が定義されています。原型は型引数を伴わない形で 例えばListの場合は原型はListです。イレイジャと同じ形となりますがイレイジャはコンパイラによって置き換えられた形であるのに対して、原型はコンパイル前のコードで記述することができる形になるので、意味合いが異なります。また、イレイジャは全ての型に対して定義されているのに対して、原型は総称型クラスや総称型クラス配列(と特定の条件を満たす非staticなメンバクラス)のみに対して定義されているものになります。

原型は 主に総称型が導入される前のコードとのコードレベルの互換性を保つために利用されます。(イレイジャはバイナリレベルの互換性を保つ役割を果たします。)そのため、J2SE 5.0以降で新たにコードを書く場合は、基本的にパラメータ化された型(List<String>、List<Integer>など)や型引数を伴う型(List<E>など)を使用します。原型を使うと総称型の型安全の仕組みを簡単に崩すことができてしまうので、使用する場合には注意が必要です(原型を使用するとコンパイラが警告を出します)。

原型は通常のコーディングでは使用しないものの、いくつか原型を指定しないといけない場面があります。

原型を指定する場面

原型を指定しないといけない場面は2つあります。

  • クラスリテラルを指定する場合
    クラスリテラルはString.class、Integer[].class、int.classのように指定しますが、総称型の場合はList<String>.classやList<?>.classのようには指定できず、List.classと原型を指定します。
  • instanceof でクラスを指定する場合
    instanceofの右辺は List<String>やList<T>のようには指定できず、Listと原型を指定します。instanceofでは非境界ワイルドカードList<?>を指定することもできますが、原型を指定する場合と変わりません。
// クラスリテラル
Class<?> c1 = String.class;
Class<?> c2 = String[].class;
Class<?> c3 = List.class;
// Class<?> c4 = List<String>.class; // この指定はコンパイルエラー
// Class<?> c5 = List<?>.class;      // 同様

// instanceof
Object o = new Object();
if (o instanceof String) {}
if (o instanceof String[]) {}
if (o instanceof List) {}
// if(o instanceof List<String>) {} // この指定はコンパイルエラー
if (o instanceof List<?>) {}        // これはOKだが、Listを指定した場合と同じ

変性

型引数の継承関係総称型の継承関係連動性を変性と言います。Javaの総称型の変性は非変です(一般的には不変と呼ばれますが、不変(immutable)と区別するために このサイトでは「非変」と呼ぶことにします)。

例えばjava.uitl.Listクラスで型引数としてNumberとIntegerを指定した場合を例に挙げて説明します。NumberとIntegerの間には継承関係があり、IntegerはNumberのサブクラスです。Numberとして宣言した変数にはIntegerのインスタンスを格納することができます。

Number num1 = Integer.valueOf(123);
Number num2 = 12345;  // オートボクシングでIntegerに変換

一方でList<Number>とList<Integer>の関係に着目すると、型引数NumberとIntegerの間に継承関係があるため、直感的にはList<Number>として宣言した変数にはList<Integer>のインスタンスを格納できそうなのですが、Javaではこれができません。この関係を非変(invariant)と言います。

List<Integer> intList = new ArrayList<>();
//List<Number> numList = intList;    // 非変のためコンパイルエラー

もし、List<Integer>がList<Number>のサブタイプとして扱えれば、その関係を共変(covariant)と言います。逆に 要素の継承関係と反対にList<Number>がList<Integer>のサブタイプとして扱えれば、その関係を反変(contravariant)と言います。

非変は直感的でないようにも思えるかも知れませんが共変は型安全を損なう危険性があります。「配列」の章で共変である配列で型安全を損なう例を挙げましたが再掲します。

Object[] objArray = new Integer[3];   // objArrayはObject[]と宣言されているが、実体はInteger[]
                                      // 配列は共変であるため、Object[]の変数にInteger[]を格納することが可能。
objArray[0] = "Hello World!";  // Integer[]にStringの要素を代入しようとしている。
                               // コンパイルは通るが 実行時にArrayStoreExceptionが発生する。

総称型は非変とすることにより配列のような型安全を損なうコードは書けないようになっています。しかし、共変は時に便利なことがあります。例えばList<Number>を考えます。型引数をNumberとしたため、add()メソッドはNumberクラスのインスタンスを引数に取ることができ、サブクラスのIntegerやDoubleのインスタンスをリストに追加することができます。そうすると、addAll()メソッドでもList<Number>だけでなくList<Integer>やList<Double>も渡せた方が直感的で柔軟性が上がります。

Javaの総称型は非変ですが、このように共変が実現できると便利な場面もあるため、共変を実現するメカニズムが用意されています。また、共変だけでなく反変を実現するメカニズムも用意されています。総称型の変性を指定することを変位指定と呼びます。

変性
変性上述の例Javaでの変位指定
非変(invariant)List<Number>とList<Integer>の間に継承関係はない<T>
共変(covariant)List<Integer>がList<Number>のサブタイプとして扱える<? extends T>
反変(contravariant)List<Number>がList<Integer>のサブタイプとして扱える<? super T>

ただし、共変には 配列の例で挙げたように型安全を損なうおそれがあるため総称型の共変では制限を加えて型安全を損なわないようにしています。Listの場合を例に挙げると、共変と指定された場合は いかなる要素も追加できないようにして型安全性を保証します。

非境界ワイルドカード型(<?>)

型引数に<?>を指定すると非境界ワイルドカード型となり 任意の型引数の総称型を格納することができます。この後に出てくる上限境界ワイルドカード型と同様に、Javaの総称型で共変を実現する手段の一つです

List<?> list = null;
list = new ArrayList<String>();    // OK
list = new ArrayList<Integer>();   // OK

非境界ワイルドカード型を渡す側の観点と受け取る側の観点で別々に見ていきます。

非境界ワイルドカード型を渡す側

public static void main(String[] args) {
    List<Integer> intList = new ArrayList<>();
    List<Number> numList = new ArrayList<>();
    List<Object> objList = new ArrayList<>();

    // 非境界ワイルドカードを渡す側。
    // どれを渡してもOK。
    handleList(intList);
    handleList(numList);
    handleList(objList);
}

public static void handleList(List<?> list) {
    ・・・
}

handleList()メソッドの引数にはList<Integer>でもList<Number>でもList<Object>でも何でも渡すことができます。

非境界ワイルドカード型を受け取る側

始めにリストから要素を取得する場合を見てみます。

public static void handleList(List<?> list) {

//  Integer i = list.get(0);    // コンパイルエラー
//  Number n = list.get(0);     // コンパイルエラー
    Object o = list.get(0);     // OK。Objectとして扱う
    if(o instanceof Integer) {
        Integer i = (Integer) o;    // クラスを検査してダウンキャストする
    }
}

引数listの具体的な型は 受け取り側では特定することができません。List<Integer>かも知れないしList<Number>かも知れないしList<Object>かも知れないし、はたまたList<String>かも知れません。ただ、どの型だとしても要素はObjectのサブクラスであることは確かなため Objectとして扱うことはできます。したがって、個々の要素はObjectとして扱うか、クラスを検査してダウンキャストをすることになります。

次にリストへ要素を追加する場合を見てみます。

public static void handleList(List<?> list) {

    // リストへの要素の追加。
    // どのクラスの要素も追加することができない。
//  list.add(Integer.valueOf(100));  // コンパイルエラー
//  list.add(Double.valueOf(3.14));  // コンパイルエラー
//  list.add(new Object());          // コンパイルエラー
    list.add(null);        // ただし null だけはOK。
}

もしlistがList<Integer>だとするとIntegerは追加できますがStringやObjectを追加することはできません。もしlistがList<String>だとするとStringは追加できますがIntegerやObjectを追加することはできません。listの型に依らず追加できる要素はないため、どの要素を追加することもできません。(ただし、nullだけは追加できます。nullは全ての型の変数に代入できるため、全ての型のサブタイプと見ることができます。)

Listの場合を例に挙げましたが、一般的に言うと非境界ワイルドカード型の引数のオブジェクト(上の例のlist)に対して、型引数を引数にとるメソッド(上の例のadd(E e)メソッド)を呼び出す場合に、null以外の値を渡すことができません。これによって 共変の場合の型安全性が損なわれる危険性を排除しています

上限境界ワイルドカード型(<? extends T>)

型引数の<? extends T>は上限境界ワイルドカード型となり、Javaの総称型で共変を実現します

List<Interger> intList = new ArrayList<Integer>();
// List<Number> numberList = intList;  // 不変のためコンパイルエラー
List<? extends Number> numberList = inList;  // 共変。OK

上の例の<? extends Number>の型引数にはNumberまたはNumberのサブクラスが当てはまります。クラスの継承ツリーを見てみると「Object – Number – Integer・Long・Doubleなど」となっていて、非境界ワイルドカードでは任意のクラスを指定できるのに対して 上限境界ワイルドカードでは「Number以下の任意のクラス」と継承ツリーの上限の型を指定することができます

上限境界ワイルドカード型を渡す側の観点と受け取る側の観点で別々に見ていきます。

上限境界ワイルドカード型を渡す側

public static void main(String[] args) {
    
    List<Integer> intList = new ArrayList<>();
    List<Number> numList = new ArrayList<>();
    List<Object> objList = new ArrayList<>();
    
    // 上限境界ワイルドカードを渡す側。
    // List<Number>かList<Numberのサブクラス>ならばOK。
    handleList(intList);    // OK
    handleList(numList);    // OK
//  handleList(objList);    // NG
}

public static void handleList(List<? extends Number> list) {
}

handleList()の引数にはList<Number>と、List<Integer>・List<Double>などList<Numberのサブクラス>であれば何でも渡すことができます。

上限境界ワイルドカード型を受け取る側

始めにリストから要素の取得をする場合を見てみます。

public static void handleList(List<? extends Number> list) {
    
//  Integer i = list.get(0);    // コンパイルエラー
    Number n = list.get(0);     // OK。Numberとして扱う。
    if(n instanceof Integer) {
        Integer i = (Integer) n;    // クラスを検査してダウンキャストする。
    }
}

引数listの具体的な型は 受け取り側では特定することができません。List<Number>かも知れないしList<Integer>かも知れないし、はたまたList<ユーザ定義のNumberのサブクラス>かも知れません。ただ、どの型だとしても要素はNumberのサブクラスであることは確実なため Numberとして扱うことができます。したがって、個々の要素はNumberとして扱うか、クラスを検査してダウンキャストをすることになります。

非境界ワイルドカード型に比べて、クラス階層の上位の型を限定することができます。

次にリストへの要素の追加する場合を見てみます。

public static void handleList2(List<? extends Number> list) {

    // リストへの要素の追加。
    // どのクラスの要素も追加することができない。
//  list.add(Integer.valueOf(100));    // コンパイルエラー
//  list.add(Double.valueOf(3.14));    // コンパイルエラー
//  list.add(new Object());            // これも勿論ダメ。
    list.add(null);        // ただし null だけはOK。
}

もしlistがList<Integer>だとするとIntegerは追加できますが DoubleやNumberやObjectを追加することはできません。もしlistがList<Double>だとするとDoubleは追加できますがIntegerやNumerやObjectを追加することはできません。listの型に依らず追加できる要素はないためどの要素を追加することもできません。(ただし、nullだけは追加できます。)

Listの場合を例に挙げましたが、一般的に言うと上限境界ワイルドカード型の引数のオブジェクト(上の例のlist)に対して、型引数を引数にとるメソッド(上の例のadd(E e)メソッド)を呼び出す場合に、null以外の値を渡すことができません。これによって 共変の場合の型安全性が損なわれる危険性を排除しています。(非境界ワイルドカード型と同様です。)

ポイントは次の2点になります。

  • List.get()のように上限境界ワイルドカード型から要素を取り出す系統のメソッド(プロデューサ・get)の場合 Numberまたはそのサブクラスとして取り出すことができ型を限定できます。
  • List.add()のように上限境界ワイルドカード型に要素を設定する系統のメソッド(コンシューマ・put)の場合は実質何もできません。

これは総称型クラスを自分で定義する際に、上限境界ワイルドカード型を使うかどうかの一つのポイントになります。この章の「境界ワイルドカード型をいつ使うか、どちらを使うか」で詳細に見て行きます。

下限境界ワイルドカード型(<? super T>)

型引数の<? super T>は下限境界ワイルドカード型となり、Javaの総称型で反変を実現します

List<Object> objList= new ArrayList<>();
// List<Number> numerList = objList;     // コンパイルエラー。
List<? super Number> numList = objList;  // 反変。コンパイル OK

上の例の<? super Number>の型引数には NumberまたはNumberのスーパークラスが当てはまります。クラスの継承ツリーを見てみると「Object – Number – Integer」となっていて非境界ワイルドカードでは任意のクラスを指定できるのに対して 下限境界ワイルドカードでは「Number以上の任意のクラス」と継承ツリーの下限の型を指定することができます

下限境界ワイルドカード型を渡す側の観点と受け取る側の観点で別々に見ていきます。

下限境界ワイルドカード型を渡す側

public static void main(String[] args) {

    List<Integer> intList = new ArrayList<>();
    List<Number> numList = new ArrayList<>();
    List<Object> objList = new ArrayList<>();

    // 下限境界ワイルドカードを渡す側。
    // List<Number>かList<Numberのスーパークラス>ならばOK。
//  handleList(intList);    // NG
    handleList(numList);    // OK
    handleList(objList);    // OK
}

public static void handleList(List<? super Number> list) {
}

handleList()の引数にはList<Number>かList<Object>のどちらかを渡すことができます。

下限境界ワイルドカード型を受け取る側

始めにリストから要素の取得をする場合を見てみます。

public static void handleList(List<? super Number> list) {
    
//  Number n = list.get(0);    // コンパイルエラー
    Object o = list.get(0);    // OK。Objectとして扱う。
    if(o instanceof Number) {
        Number n = (Number) o;    // クラスを検査してダウンキャストする。
    }
}

引数listの具体的な型はList<Number>かList<Object>のいずれかに限定されます。NumberとObjectを共通的に扱うにはObjectとして扱うことになります。したがって、個々の要素はObjectとして扱うか、クラスを検査してダウンキャストをすることになります。

これは非境界ワイルドカード型と同様で 型を限定することができません。

次にリストへの要素の追加する場合を見てみます。

public static void handleList(List<? super Number> list) {
    // リストへの要素の追加。
    // listがList<Number>だとすると、IntegerやNumberは追加できるがObjectは追加できない。
    // listがList<Object>だとすると、何でも追加できる。
    // listの型に依らずNumberおよびサブクラスは追加することができる。
    list.add(Integer.valueOf(100));    // OK
    Number n = Double.valueOf(3.14);
    list.add(n);    // OK
//        list.add(new Object());    // コンパイルエラー
    list.add(null);        // 勿論 null もOK。
}

もしlistがList<Number>だとするとNumberやIntegerは追加できますがObjectを追加することはできません。もしlistがList<Object>だとするとどんなクラスインスタンスも追加することができます。listの型に依らずNumberおよびそのサブクラスを追加することができます

非境界ワイルドカード型や上限境界ワイルドカード型と違って、限定された型の要素を追加することができます

ポイントは次の2点になります。

  • List.get()のように下限境界ワイルドカード型から要素を取り出す系統のメソッド(プロデューサ・get)の場合 Objectとして取り出すことになり型を限定できません。
  • List.add()のように下限境界ワイルドカード型に要素を設定する系統のメソッド(コンシューマ・put)の場合はNumberおよびそのサブクラスを渡すことができます。

これは総称型クラスを自分で定義する際に 下限境界ワイルドカード型を使うかどうかの一つのポイントになります。この章の「境界ワイルドカード型をいつ使うか、どちらを使うか」で詳細に見て行きます。

List、List<Object>、List<?>の違い

List(原型)とList<Object>とList<?>は 全てイレイジャが同じListになりますが、意味は全く異なります。違いをまとめると次のようになります。

List、List<Object>、List<?>の違い
格納されている要素格納できる要素変性型安全性と根拠
List任意任意共変・反変危険(共変で、要素を格納できる)
List<Object>任意任意非変安全(非変である)
List<?>任意nullのみ共変安全(共変だが、要素を格納できない)

実行時の型安全を損なう根本の原因は共変です。配列は共変であるため 実行時の型安全を保証することができません。総称型は非変ですが、原型のListは共変(および反変)です。( List<String>やList<Integer>などのパラメータ化された型を代入することができます。)そのため、原型のListも配列と同様 型安全を保証することができません。それに対してList<Object>は任意の要素を格納することができますが、非変のため型安全を保証することができます。また、List<?>は共変ではありますが、List<?>に要素を追加することができないという制限があるため 型安全性を保証することができます。

大雑把に言うと、任意の要素を格納したい場合はList<Object>を使い何が格納されているか分からないリストを受け取る場合はList<?>を使います原型のListは J2SE 5.0以前のコードと互換性を保つ目的以外では利用を避けるべきです

再帰型境界

頻度は多くはないのですが、型引数が その型引数自身が関係する何らかの式で制限される場合があります。これを再帰型境界と呼びます。代表的な例は次の2つです。

  • Comparableインタフェース
  • 擬似自分型(simulated self-type)

それぞれ詳しく見てみます。

Comparableインタフェース

Comparableは Comparable<T>のように総称型インタフェースとして定義されていて、compareTo()メソッドを実装する必要があります。Comparable<T>を実装するとT型のインスタンスと比較可能になるわけですが、大抵の場合は自分自身のクラスのインスタンスとだけ比較可能で 要素間の順序付けを行う場合に利用されます。例えばIntegerはComparable<Integer>を実装し、StringはComparable<String>を実装しています。これはそれぞれ次のようなクラス定義になります。

  • class Integer implements Comparable<Integer> { … }
  • class String implements Comparable<String> { … }

これを一般化して型変数で表すと T extends Comparableと再帰的な形の再帰型境界になるわけです。CollectionsのクラスメソッドなどでComparableの再帰型境界が多用されています。そのうちの1つを見てみます。

public static <T extends Comparable<? super T>> void sort​(List<T> list)

型引数が <T extends Comparable<? super T»となっていて <T extends Comparable<T»の部分は上の形と同じで 自身と比較可能なT型となります。sort()メソッドでは更に2番目のTが下限境界ワイルドカード型になっています。これは次のような理由によります。

Comparableを実装したクラスSuperがあるとします。これはSuper extends Comparable<Super>であり、他のSuperと比較するcompareTo()メソッドを実装しています。そして SuperのサブクラスSubを定義する場合に、比較処理が変わらないためcompareTo()をオーバーライドする必要がなかったとします。すると SubクラスはSub extends Comparable<Super>になってしまい、他のSubと比較することができません。この状況に対応するためには、Sub extends Comparable<? super Sub>とする必要があります。これを一般化して型変数で表すと T extends Comparable<? super T>になります。

余談:Collections.max()の型引数

Collectionsには更に複雑な型引数のメソッド、max()があります。


public static <T extends Object & Comparable<? super T>> T max​(Collection<? extends T> coll)

T extends Comparable<? super T>に加えて extendsの後に”Object &“が追加されています。extends の後にはTが継承すべき親クラスや実装すべきインタフェースを’&’で繋げて列挙することができます。つまり、TはObjectを継承している必要があり、Comparable<? super T>を実装している必要があるという制約を表しています。

しかし、Objectを継承する必要があるという制約は冗長なように感じられると思います。実際、max()メソッドの型引数から”Object &”を取ってしまって次のようにしても 問題なく動作します。

②public static <T extends Comparable<? super T>> T max​(Collection<? extends T> coll)

何故”Object &”が必要かというと、総称型導入前のAPIとの互換性を取るためです。extends の後にクラスやインタフェースを&で繋げて列挙する場合、最初のクラスやインタフェースがイレイジャに採用されるルールになっています。

従って ①のイレイジャは次のようになります。public static Object max​(Collection coll)

一方で ②のイレイジャは次のようになります。public static Comparable max​(Collection coll)

総称型導入前のmax()メソッドの定義は public static Object max​(Collection coll) であり、②では整合が取れないため max()メソッドの定義は①のようになっているというわけです。

擬似自分型

Builderパターンやclone()メソッドなど いくつかの状況において自分の型を返したい場合があります。このような場合にサブクラスを作成すると、サブクラスのメソッドの戻り型がスーパークラスの型になってしまい、オーバーライドをしない限りサブクラスの型を返すことができません。Builderパターンの場合を例に挙げます。

// Person クラス
class Person {
    String name;
    
    Person(String name){
        this.name = name;
    }
    
    public String toString() {
        return "Person[name=" + name + "]";
    }
}

// Person ビルダ
class PersonBuilder {
    
    protected String name;
    
    public PersonBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public Person build() {
        return new Person(name);
    }
}

// 利用例
public static void main(String[] args) {
    Person bob = new PersonBuilder()
            .withName("Bob")
            .build();
    System.out.println(bob);    // Person[name=Bob]
}

Personを継承したサブクラスStudentと Studentを返すStudentBuilder(PersonBuilderを継承)を作成するとします。それぞれを単純に継承した場合、ビルダがスーパークラスの型を返してしまい、整合を取ることができません。

// Student クラス。Person から派生
class Student extends Person {
    int grade;

    Student(String name, int grade) {
        super(name);
        this.grade = grade;
    }
    
    public String toString() {
        return "Student[name=" + name + ",grade=" + grade + "]";
    }
}

// Student ビルダ。PersonBuilder から派生
class StudentBuilder extends PersonBuilder {
    
    private int grade;
    
    public StudentBuilder withGrade(int grade) {
        this.grade = grade;
        return this;
    }
    
    public Student build() {
        return new Student(name, grade);
    }
}

// 利用例
public static void main(String[] args) {
    // コンパイルエラー
//   Student jack = new StudentBuilder()
//           .withName("Jack")    // withName()の戻り型はPersonBuilder
//           .withGrade(3)        // PersonBuilderはwithGrade()を定義していない
//           .build(); 
//   System.out.println(jack);
}

withName()メソッドの戻り型がPersonBuilderであるため、withGrade()メソッドを呼び出すことができません。サブクラスにおいて「実行時の自分の型を返す」という指定(自分型)ができれば良いのですが、Javaではこのような指定方法がありません。

解決方法の1つとして StudentBuilderクラスでwithName()メソッドをオーバーライドして、戻り型をStudentBuilderにするという方法が考えられます。しかし、この方法はオーバーライドすべきメソッドが増えてくると 冗長なボイラープレートコードが増えてしまい あまり良い解決策とは言えません。

別の解決方法は、総称型を利用して擬似自己型とすることです。PersonBuilderとStudentBuilderを擬似自己型に置き換えると次のようになります。(PersonクラスとStudentクラスは変更ありません。)

// PersonBuilderを総称型に。
class PersonBuilderGen<T extends PersonBuilderGen<T>> {
    
    protected String name;
    
    public T withName(String name) {
        this.name = name;
        return (T) this;    // 警告:未検査キャスト
    }
    
    public Person build() {
        return new Person(name);
    }
}

// PersonBuilder から派生
class StudentBuilderGen extends PersonBuilderGen<StudentBuilderGen> {
    
    private int grade;

    public StudentBuilderGen withGrade(int grade) {
        this.grade = grade;
        return this;
    }
    
    public Student build() {
        return new Student(name, grade);
    }
}

// 利用例
public static void main(String[] args) {
    Student jack = new StudentBuilderGen()
            .withName("Jack") // StudentBuilderGenを返す。
            .withGrade(3)     // OK。
            .build();         // OK。Studentを返す。
    System.out.println(jack);
}

クラス定義について説明します。PersonBuilderGenのwithName()メソッドはT型を返します。そのため、型引数TはPersonBuilderGenのサブクラスであるという制限を設けます。そのため、PersonBuilderGen<T extends PersonBuilderGen<T»となります。再帰的な定義で目が回ってしまいそうですが、StudentBuilderGenがまさにT extends PersonBuilderGen<T>の形になっています。

続いて利用例について説明します。PersonBuilderGenのwithName()メソッドの戻り値はTで、コンパイル時にイレイジャObjectになってしまいますが、同時に呼び出し側にTの実際の型であるStudentBuilderGenへのキャストが挿入されます。従って withName()の戻り値はStudentBuilderGenにキャストされるため、続けてwithGrade()メソッドを呼び出すことができます。

尚、上の例ではPersonBuilderのwithName()メソッドのreturnの部分で未検査キャストの警告が出ます。動作には問題ないのですが、この警告が出ないような実装にすることも可能です。Effective Javaの中で紹介されているように、PersonBuilderを抽象クラスとして、自身を返す抽象メソッド self()を定義し withName()のreturn部分を return self();に置き換えます。self()を抽象メソッドとすることによりサブクラスにおいてself()メソッドのオーバーライドを強制します。この仕組みにより 未検査キャストの警告を出すことなく 擬似自己型を実現することができます。

総称型配列

配列は共変で 総称型は非変です。総称型はコンパイル時に型安全性の検査を行い、配列は実行時に型安全性の検査を行います(安全でない場合は ArrayStoreExceptionなどの例外を投げます)。このような違いから配列と総称型は相性が悪く総称型の配列(List<E>[]やList<String>[]、T[]など)を明示的に生成できないようになっています。(暗黙的に生成する方法はあります。後述します。)

仮に 総称型の配列が生成できるとすると、配列の共変の危険性とイレイジャによる型情報の欠落が加わって より複雑な問題を引き起こす可能性があります。次に例を挙げます。次のコードは実際にはコンパイルが通りません。(①がコンパイルエラーになります。)

List<Integer>[] intListArray = new List<Integer>[1];  // ①実際にはnewできない。
List<String> strList = List.of("One", "Two");
Object[] objArray = intListArray;   // 配列は共変なのでOK。実際の型はList<Integer>[]
objArray[0] = strList;  // List<Integer>の配列の0番目にList<String>を代入。
                        // 本来はここで ArrayStoreExceptionが発生して欲しいところだが
                        // どちらもイレイジャが List のため正常に格納できてしまい 例外は発生しない。
int i = intListArray[0].get(0);  // intListArray[0]にはstrListが格納されている。
                                 // "One" を int にキャストできないので ClassCastExceptionが発生。

このような潜在的な危険性があることから、総称型配列は明示的に生成できないようになっています。例外的に非境界ワイルドカード型の配列(List<?>[]など)は生成できますが、List<?>には実質的に要素が追加できないため有用な利用ケースは限られそうです。

このように、パラメータ化された型(上の例ではList<Integer>)に対して 違う型の要素(上の例ではString)が(ArrayStoreExceptionが発生せずに)格納されてしまうことをヒープ汚染(heap pollution)と呼びます。上の例では配列の共変とイレイジャの型情報の欠落がヒープ汚染を発生させる原因となっています。ヒープ汚染は原型を利用することによっても発生し得ます。

List list = new ArrayList<Integer>();  // 警告が発生(raw型を使用)
List<String> strList = list;           // 警告が発生(未検査の型変換)
list.add(Integer.valueOf(123));        // 警告が発生(raw型を使用)
String str = strList.get(0);           // ClassCastExceptionが発生

Listのような原型の変数にはList<String>のようなパラメータ化された型を代入することができます(警告は出ます)。逆にパラメータ化された型の変数には原型を代入することもできます(同様に警告は出ます)。原型はいわば 共変と反変の性質を持っているため、配列同様 共変による型安全を損なう危険性を持ち合わせています

総称型の可変長引数

総称型配列を明示的に生成することはできませんが、暗黙的に生成することはできます。メソッドの可変長引数は実際には配列として渡されるため、メソッドの引数を総称型の可変長引数にすると メソッドには総称型配列が渡されることになります。そうなると、前述の例のようなヒープ汚染を引き起こすコードが実際に書けてしまいます。そのため、メソッドの引数と総称型の可変長引数にすると、コンパイラが潜在的なヒープ汚染の可能性がある旨の警告を発します。メソッドを呼び出す側にも警告が表示されます。

前の節で挙げたコンパイルエラーになるヒープ汚染の例が、総称型の可変長引数を渡すことにより実際に書けてしまいます。

public void someMethod(List<Integer>... intListArray) {  // 実際にはList[]として渡される。
    List<String> strList = List.of("One", "Two");
    Object[] objArray = intListArray;   // 配列は共変なのでOK。実際の型はList[]
    objArray[0] = strList;  // Listの配列の0番目にList<String>を代入。
                            // 本来はここで ArrayStoreExceptionが発生して欲しいところだが
                            // どちらもイレイジャが List のため正常に格納できてしまい 例外は発生しない。
    int i = intListArray[0].get(0);  // intListArray[0]にはstrListが格納されている。
                                     // "One" を int にキャストできないので ClassCastExceptionが発生。
}

ヒープ汚染を発生させないようにするには、引数とした受け取った総称型配列は あくまでも読み出しのみとし、格納や変更といった操作を行わないようにすることです。

総称型の可変長引数には ヒープ汚染ともう一つ別に注意すべき点があります。可変長引数は配列を生成してメソッドに引き渡しますが、生成する配列の型は実行時ではなくコンパイル時に決定されます。可変長引数をそのまま返すメソッドを例に挙げます。

private static <T> T[] toArray(T... args) {
    System.out.println(args.getClass());  // class [Ljava.lang.String;
    return args;
}

public static void main(String[] args) {
    String[] strArray = toArray("One", "Two", "Three");
    System.out.println(Arrays.toString(strArray));      // [One, Two, Three]
}

可変長引数に複数の文字列を渡すとtoArray()メソッドではString[]を受け取り、String[]を返します。

次にワンクッション挟んでtoArray()メソッドを呼び出します。

private static <T> T[] toArray(T... args) {
    System.out.println(args.getClass());  // class [Ljava.lang.Object;
    return args;
}

private static <T> T[] cushion(T a, T b, T c) {
    System.out.println(a.getClass());   // class java.lang.String
    return toArray(a, b, c);
}

public static void main(String[] args) {
    String[] strArray = cushion("One", "Two", "Three"); // ClassCastException。Object[]が返ってくるため。
    System.out.println(Arrays.toString(strArray));
}

メソッドcushion()ではStringとして引数を受け取っているにも関わらず、toArray()にはObject[]が渡され、toArray()の戻り値もObject[]になってしまいます。その結果、cushion()の戻り値もObject[]となり、大元の呼び出し元のmain()メソッドでClassCastExceptionが発生します。これはcushion()の引数Tがコンパイル時にイレイジャObjectに置き換えられ、コンパイル時の型を元にtoArray()に渡す型を決定しているためと考えられます。

呼び出し側で期待する動作から外れてしまうため、基本的には総称型の可変長引数を受け取った場合は、その変数を呼び出し側には返さないようにするのが無難です。

@SafeVarargsアノテーション

総称型の可変長引数は有用ではありますが、型安全を損なう可能性があるため、次の2点に注意する必要があることを見てきました。

  • 受け取り側では基本的に読み出し専用として、格納や変更といった操作を行わない。
  • 受け取った総称型配列を呼び出し側に返さない

この2点を守り、総称型の可変長引数に危険性がないことの確認が取れていれば、わずらわしい警告を抑制することができます。メソッド定義に @SafeVarargsアノテーションを付けることによって 総称型可変長引数を定義する側および呼び出す側の警告を抑制することができます。

実行時の型検査

総称型はイレイジャで実現されていて、コンパイル時に型検査を行います。実行時には型の情報を保持していないため、実行時に宣言と異なる型の要素を渡されると型検査をすり抜けてしまいます。原型を用いると そのような状況を簡単に作り出すことができてしまいます。

List<String> strList = new ArrayList<>();
// 原型の変数を介することによって、警告は出るがコンパイルは通ってしまう。
List rawList = strList;
rawList.add(Integer.valueOf(100));    // 型が違うが、ここでは例外は出ない。
String str = strList.get(0);          // ここでClasCastException発生。

本来であれば 型が異なる要素を追加したところでエラーを検出して欲しいのですが、実際には要素を取り出す際に例外が発生します。上の例では格納後すぐに取り出しが行われていますが、格納と取り出しが離れているような場合には 問題の原因を分かりづらくしてしまいます。

総称型のクラスに型情報を持たせることによって 実行時の型検査を行うことができます。「クラス」の章の「継承に代わるコード再利用のパターン:コンポジション」で取り上げたコンポジションを簡略したパターンを適用して 型情報を保持する 次のようなクラスを定義します。

class TypeSafeList<E> implements List<E> {
    
    private final List<E> list = new ArrayList<>();
    private final Class<E> type;  // 型トークン
    
    public TypeSafeList(Class<E> type) {
        this.type = type;
    }

    @Override
    public boolean add(E e) {
        // コンテナに要素を格納するメソッドで実行時の型検査を行う。
        return list.add(type.cast(e));
    }

    // 転送
    @Override
    public int size() {    return list.size();    }

    @Override
    public boolean isEmpty() { return list.isEmpty(); }

    // … 同様
}

このクラスではコンテナに要素を格納するメソッドにおいて Classクラスのcast()メソッドによる型検査を行います。これによって 宣言と異なる型の要素を格納しようとすると、実行時例外を発生させることができます。

List<String> strList = new TypeSafeList<>(String.class);
List rawList = strList;
rawList.add(Integer.valueOf(100));    // ここでClasCastException発生。
String str = strList.get(0);

最初の例と同様 ClassCastExceptionが発生しますが、要素を取り出す時ではなく、要素を格納する時に型検査を行うことができます。このように、総称型クラスの型情報を表す Classクラスの情報は型トークン(type token)と呼ばれます。境界ワイルドカード型などで境界を指定する場合は境界型トークン(bounded type token)と呼ばれ、非境界ワイルドカード型とする場合は非境界型トークン(unbounded type token)と呼ばれます。

java.util.Collectionsでは 上の例と同じように 指定したコレクションに対して 実行時の型検査を行うビューを返すユーティリティメソッドを用意しています。checkedList()、checkedSet()、checkedMap()などです。checkedList()を利用すると 上の例と同じような結果になります。

List<String> strList = Collections.checkedList(new ArrayList<>(), String.class);
List rawList = strList;
rawList.add(Integer.valueOf(100));    // ここでClasCastException発生。
String str = strList.get(0);

余談ですが、総称型が導入される際には 総称型クラスのコンストラクタに型トークンを渡す方式も検討されたようです。しかし、そうすると総称型導入前のコレクションクラスと互換性が取れなくなってしまうため 型トークン方式は採用されずイレイジャ方式が採用されたようです。

Javaの理論と実践: Generics、了解!」(IBM Developer)

総称型クラスや総称型メソッドの作成

自分で総称型のクラスを定義する場合を見て行きます。クラス自体を総称型にする他に メソッドだけ総称型にしたり、コンストラクタだけ総称型にすることもできます。

総称型クラスの定義と利用

総称型のクラスを定義する場合を見てみます。

public class SomeClass<T> {
    private T someField;
    public void setSomeField(T param) {
        this.someField = param;
    }
    public T getSomeField() {
        return this.someField;
    }
}

Tは型引数で 慣例的に「T」(Type)、「E」(Element)、「K」(Key)、「V」(Value)などの一文字が良く使われます。クラス定義の中で総称型の型を指定する箇所を型引数で記述します。型引数にはワイルドカード(<?>、<? extends T>、<? super T>)を指定することもできます。

定義した総称型クラスは、クラスライブラリの総称型クラスと同じ要領で利用することができます。

// 型引数をStringにした場合
SomeClass<String> someStr = new SomeClass<>();
someStr.setSomeField("message");
System.out.println(someStr.getSomeField());

// 型引数をIntegerにした場合
SomeClass<Integer> someInt = new SomeClass<>();
someInt.setSomeField(123);    // プリミティブ型はラッパークラスに変換される。(オートボクシング)
System.out.println(someInt.getSomeField()); // 逆変換。(オートアンボクシング)

総称型メソッドの定義と利用

クラス全体を総称型とせずに メソッドを総称型とする場合の定義の仕方を見てみます。型引数はメソッドの戻り値の直前に記述します

class SomeClass2{
    public <T> T someInstanceMethod(T param) {
        return param;
    }

    public static <T> T someStaticMethod(T param) {
        return param;
    }
}

定義した総称型メソッドは、クラスライブラリの総称型メソッドと同じ要領で利用することができます。

// インスタンスメソッド
SomeClass2 someCls = new SomeClass2();
System.out.println(someCls.someInstanceMethod("message1"));    // 型名を指定する必要がない。
System.out.println(someCls.<String>someInstanceMethod("message2"));    // 型名を指定してもエラーにはならない。

System.out.println(someCls.someInstanceMethod(12345));    // 型名を指定する必要がない。
System.out.println(someCls.<Integer>someInstanceMethod(67890));    // 型名を指定してもOK。

// クラスメソッドも同様
System.out.println(SomeClass2.someStaticMethod("message3"));
System.out.println(SomeClass2.<String>someStaticMethod("message4"));
System.out.println(SomeClass2.someStaticMethod(12345));
System.out.println(SomeClass2.<Integer>someStaticMethod(67890));

総称型コンストラクタの定義と利用

総称型メソッドの定義と同じように、コンストラクタのみに総称型を適用することもできます。ただ コンストラクタの引数はインスタンスフィールドに設定することが多く、その場合はたいていクラスを総称型としてしまうので コンストラクタのみを総称型とするケースは多くはないと思います。

class SomeClass3 {
    
    private String className;
    
    // コンストラクタのみを総称型にする
    public <T> SomeClass3(T param) {
        className = param.getClass().getSimpleName();
    }
    
    public String getClassName() {
        return className;
    }
}

定義した総称型のコンストラクタは総称型のメソッドと同じ要領で利用することができます。

SomeClass3 someStr1 = new SomeClass3("string"); // 型名を指定する必要がない。
System.out.println(someStr1.getClassName());    // "String"

SomeClass3 someStr2 = new<String> SomeClass3("string");    // 型名を指定しても良い。
System.out.println(someStr2.getClassName());    // "String"

SomeClass3 someInt = new SomeClass3(12345);
System.out.println(someInt .getClassName());    // "Integer"

ワイルドカード型の利用

ライブラリを設計する場合は 柔軟性を高めるために積極的に境界ワイルドカード型の利用を検討すべきと言われています。ただし、ワイルドカード型を適用するのは クラスの型引数やメソッドの引数に対してで、基本的に戻り値にはワイルドカード型は適用しません

境界ワイルドカード型をいつ使うか、どちらを使うか

これまでの中でもいくつか例を見てきましたが、境界ワイルドカード型を利用するとライブラリの柔軟性を上げることができます。総称型のメソッドの引数を境界ワイルドカード型にするかどうかの指針として次の2つがあります。

  1. PECS(Producer – Extends, Consumer – Super)
  2. getとputの原則

引数から値などを取り出す操作(Producer、get)だけを行う場合は<? extends T>を、引数に値を渡して格納したり処理させる操作(Consumer、put)だけを行う場合は<? super T>を、両方行う場合は境界ワイルドカード型は使わないという原則です。

これを端的に表しているのは次の例です。

public static<T> void copy(List<? extends T> from, List<? super T> to) {
    to.put(from.get());
}

fromから要素を取り出すため<? extends T>としていて、toに要素を渡すため<? super T>としています。fromは共変のため、fromに要素を格納することはできませんが fromからTまたはTのサブタイプの要素を取り出すことができます。一方でtoは反変であり TまたはTのスーパータイプのコンテナであるため、TまたはTのサブタイプの要素を格納することができます。

もう一つの例はCollections.max()メソッドです。メソッド定義は次のようになっています。

public static <T> T max​(Collection<? extends T> coll, Comparator<? super T> comp)

内部の処理をイメージすると分かり易いのですが collから要素を順々に取り出して compに渡して比較して 大きい方をmaxの候補として残し 次の要素と比較していきます。collから要素を取り出すため<? extends T>としていて、取り出した要素をcompに渡すため<? super T>としています。

型引数と非境界ワイルドカード型のどちらを使うか

Collectionsのクラスメソッドswap()は 指定したリストの指定した位置の要素を入れ替えるメソッドです。メソッド定義は次の通りです。

① public static void swap(List<?> list, int i, int j);

これは、次のような定義にしても同じことになります。

② public static <E> void swap(List<E> list, int i, int j);

ただし、swap()メソッドの中で、②ではlistの要素の入れ替えが行えるのに対して、①ではlistの要素の入れ替えが行えません。そのため ①の内部で②の名前を変えたprivateなメソッド(ヘルパメソッド)を呼び出すというテクニックが知られています。(実際のCollections.swap()メソッドでは メソッド呼び出しのオーバーヘッドを無くすため List<?> listを原型Listの変数に代入することにより、ヘルパメソッドを呼び出さずに入れ替えを実現しています。)

ライブラリを設計する上では メソッドの型引数が 他の引数・戻り値・クラスの型引数のいずれとも一致しない場合は非境界ワイルドカード型にした方が良いとされています。②の例ではlistの型引数E以外に型引数が登場しないため 非境界ワイルドカード型にするのが良いとされます。

一方で非境界ワイルドカード型にできない例を見てみます。

他の引数の型引数と一致している例

Collectionsのクラスメソッドfill()を例に挙げます。メソッド定義は次の通りです。

public static <T> void fill(List<? super T> list, T obj);

listはobjを格納するために、T型かT型のスーパークラスである必要があります。listのTとobjのTは同じ型を指しているので 非境界ワイルドカード型にすることはできません。

戻り値の型引数と一致している例

同じくCollectionsのクラスメソッドlist()メソッドを例に挙げます。メソッド定義は次の通りです。

public static <T> ArrayList<T> list(Enumeration<T> e);

eが列挙したT型の要素を含むArrayList<T>を返します。eのTと戻り値のTは同じ型を指しているので 非境界ワイルドカード型にすることはできません。

クラスの型引数と一致している例

List<E>のメソッドadd()メソッドを例に挙げます。メソッド定義は次の通りです。

boolean add(E e);

eの型Eはクラスの型引数Eを指しているので、非境界ワイルドカード型にすることはできません。

型引数が複数登場するが非境界ワイルドカード型を使った例

Collectionsのクラスメソッドdisjoint()を例に挙げます。メソッド定義は次の通りです。

public static boolean disjoint(Collection<?> c1, Collection<?> c2);

c1とc2に共通の要素が存在すればtrue、存在しなければfalseを返します。c1とc2の要素の型は一致していなくても構わず 関連性を限定する必要がないことから、どちらも非境界ワイルドカード型とすることができます。

型引数でできないこと

型引数では次のようなことができません。

  • new演算子によるインタスタンス生成
  • instanceofによるクラス判定
  • クラスリテラル(.class)によるClassオブジェクトへのアクセス
  • クラスメンバからクラスの型引数にアクセスすることはできない

それぞれざっと見て行きます。

new演算子によるインスタンス生成

// T t = new T();  // コンパイルエラー

仮にnew T()ができると、TがイレイジャObjectに置き換えられ 実際に生成されるtはObject型になってしまうため、T型を戻すメソッド(T get();など)で呼び出し側のキャストが失敗してしまいます。そのため、このようなコードは書けないようになっていると思われます。

C#の総称型も参照型についてはイレイジャに近い方式をとっていますが、(一定の制約の元で)new T()でT型のインスタンスを生成することができます。これは、C#ではJavaと違って 実行時にTの型情報を取得することができ、実行時の型に合わせたインスタンスを生成することができるためです。ただし、new T()できるようにするためには new制約が必要で、Tに指定できるのは引数なしのコンストラクタを持つクラスに限定されます。

instanceofによるクラス判定

// コンパイルエラー
// if(t instanceof T) {
//     処理;
// }

TがイレイジャObjectに置き換えられ、期待された動作(実行時のTのインスタンスであるかどうかの判定)にならないため、誤用されないように このコードは書けないようになっていると思われます。

.classによるClassインスタンスへのアクセス

// Class cls = T.class;  // コンパイルエラー

同様にTがイレイジャObjectに置き換えられ、期待された動作(実行時のTのクラスを取得)にならないため、誤用されないように このコードは書けないようになっていると思われます。

クラスフィールドやクラスメソッドでクラスの型引数を使うことはできない

クラスを総称型にした場合、クラスフィールドの型を型引数にすることはできません。また、クラスメソッドの引数や戻り値の型を型引数にしたり、メソッド内で型引数にアクセスすることはできません。

class SomeClass<T> {
    // インスタンスフィールド
    private T someValue;  // OK
    // クラスフィールド
//  private static T someClassValue;  // コンパイルエラー
    // インスタンスメソッド
    public T getSomeValue() {  // OK
        return someValue;
    }
    // クラスメソッド
    // コンパイルエラー
//  public static T getSomeClassValue() {
//      return someClassValue;
//  }

    public static void someStaticMethod() {
//      T value;    // 型引数にアクセスできない
    }
}

例えばクラスフィールドの型を型引数にできてしまうと、SomeClass<Integer>のインスタンスを生成するとsomeClassValueはInteger型となり、SomeClass<String>のインスタンスを生成するとsomeClassValueはString型となり、同じフィールドなのに型が異なるという矛盾が生じてしまいます。

そのため、クラスフィールドやクラスメソッドでは インスタンスごとに異なる型引数を使うことはできないようになっています。