テックサーチ

技術周りの記事を書いていきます

Java総称型のワイルドカードを上手に使いこなすための勘所

Java総称型のワイルドカードを上手に使いこなすための勘所

Java5以降では総称型(generics)がJava言語に導入されています。総称型自体は、最近の静的な型付けのプログラミング言語で珍しいことではなく、現在の最新版では.NETのC#Visual Basicにも導入されています。一般的には総称型をサポートするクラスライブラリを自分で正しく定義することは非常にスキルがいるが、事前に定義されたクラスを使うだけであれば、それほど難しくないとされています。しかし、Java言語の総称型は本エントリで説明するように特殊なところがあり、単に利用するだけでも他の言語に比べて遥かに難しいところがあるというのも事実です。特に総称型をパラメータ化する際に指定するワイルドカード型(List<? extends Serializable>など)の意味を正しく理解して使いこなすことは簡単なことではありません。その結果、昔のJDK1.4までのように型パラメーターのないraw型を使ってしまったり、不適切に警告を無視してしまい、総称型の提供する型安全性を正しく使いこなせていないプロジェクトも多いというのが実情なのではないかと思います。ここでは事前にList<E>などのように定義された総称型を使う際に、理解が困難なワイルドカード型を正しく使うための勘所について簡単にまとめてみたいと思います。

Javaの変性は配列→共変、総称型→非変に固定されている

オブジェクト指向言語などポリモーフィズム多態性)をサポートする言語で総称型を利用する場合には、「総称型変数の型」と「総称型そのもの型」の間で成立する型の親子関係に注意する必要があります。これは総称型変数に対する変性と呼ばれており以下の3つの種類があります。

総称型A<T>に対して、型変数Tに与える型PとCとの間でPがCのスーパータイプ(~親クラス)である場合、変性の違いによって総称型の親子関係には以下の関係が成立します。

変性 総称型の親子関係
共変(covariant) A<P>はA<C>のスーパータイプ
反変(contravariant) A<P>はA<C>のサブタイプ
非変(invariant) A<P>とA<C>との間に型の親子関係は存在しない。

【マイナビクリエイター】

ここで、Java言語ではすべての総称型は非変に固定されている(総称型変数の変位指定ができない)*1という事実を理解する必要があります。したがって、List<Object>はList<String>のスーパータイプにはなりません。よって、以下のコードはコンパイルが通りません。

List<String> strList = Arrays.asList("test1", "test2");
List<Object> objList = strList; // コンパイルエラー

objList.set(0, 3);

総称型は非変であるため、List<Object>はList<String>のスーパータイプでなく、コンパイルエラーとなって2行目の代入が認められないのです。一見制約が大きくて不便に思われるかもしれませんが、これは型安全性を保障する上で非常に大切な制約で、実際コンパイルエラーにならなかったら、最終行で別のオブジェクトの参照を通してstrListにIntegerのオブジェクトが格納されることになってしまいます。一方、普通の(業務)Javaアプリケーションでは配列をなるべく使用しない方がよい - 達人プログラマーを目指してで説明したように、配列は共変です。(配列自体は総称とは仕組みがまったく別ですが)したがって、以下のコードはコンパイル時にはエラーにも警告にもなりませんが、実行時に例外となります。つまり、コンパイル時の型安全性が保障されません。

String[] strArray = {"test1", "test2"};
Object[] objArray = strArray;

objArray[0] = 3; //java.lang.ArrayStoreException 

他の言語では変位指定できるのにどうしてJavaではできないのか

Scala言語でもデフォルトJavaと同様に非変ですが、総称型のクラスやトレイトを宣言する際に型変数に対して、変位指定を行うことで反変や共変にすることができます。

class A[T] //非変
class B[+T] //共変
class C[-T] //反変

C#でも最新の4.0からは参照型の型変数に対して変位指定が可能になっています。

public class A<T> {} //非変
public class B<out T> {} //共変
public class C<in T> {} //反変

ただし、C#の指定の仕方に示唆されているのですが、型安全性を保障するためには特定の変位の型変数に対して、インターフェース上以下の制約を満たす必要があります。

この制約は基本的にScalaでも同様です。以上の制約に違反する場合正しくコンパイルができません。通常型変数に対する変性のもともとの定義は前節で説明したような総称型の親子関係を意味するのですが、型安全性という制約を考慮すると、C#キーワードが暗示しているように共変は出力専用の型、反変は入力専用の型でなくてはならないと言い換えることができます。つまり、型安全性を保障するためには、値を取得する側の型が値を生成する側のスーパータイプになっているということが必要というわけです。この点は、型変数の変位に対して、総称型のインターフェース設計を考える上では非常に大切なポイントになってきます。

たとえば、仮にJava言語でこうした変位指定が可能だったとして、以上の制約がどうして必要なのか考えてみましょう。まず、以下の総称クラスを考えます。

public class MyBean<T> {
	private T value;
	
	public T getValue() {
		return value;
	}

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

ここで仮にTに反変性があると仮定します。さらに、これが戻り値の型として使われることが許される場合を想定します。

  MyBean<Number> a = new MyBean<Number>();
  MyBean<Integer> b = a; // 反変性のためこの代入が許される
  
  a.setValue(new BigDecimal("100"));
  Integer intValue = b.getValue(); // これを許可すると型安全でなくなる。

Tに反変性があり、かつ戻り値パラメータとしても利用可能であれば、以上のコードはコンパイルできるはずですが、実際は最終行で不正な型が代入されることになりますから、実行時例外となってしまいます。逆に、Tに共変性があった場合も同様に、

  MyBean<Integer> a = new MyBean<Integer>();
  MyBean<Number> b = a; // 共変性のためこの代入が許される
  
  b.setValue(new BigDecimal("100")); // これを許可すると型安全でなくなる。
  Integer intValue = a.getValue(); 

のような矛盾が生じます。

このように、総称型変数に共変性や反変性を持たせ、かつ、型安全にするためにはクラスのインターフェース設計に大きな制約が必要になることがわかります。(このあたりの説明については、id:Nagiseさんの以下の記事も参考になります。 ジェネリクスの代入互換のカラクリ - プログラマーの脳みそ

Java言語の哲学ではWrite Once Run Anywhereという考え方が昔からあったため、以前のバージョンで作成されたプログラムが新しいバージョンでもそのまま利用できるということが求められました。そのため、型消去(type erasure)という方策がとられただけでなく、Listなどの既存のライブラリーのインターフェースの互換性の維持にも最大限の注意が払われました。ScalaC#のように総称型パラメーターの宣言時に変位指定を可能にすることは、既存ライブラリーの設計を全面的に見直すことにつながります。それゆえ、互換性という制約を満たすためにJava言語では常に総称型変数は非変として扱われるということになったのだと思われます。*2

総称型変数の使用側で局所的に変性を変える効果のあるワイルドカード指定

このように、Java言語では既存ライブラリーの互換性維持から必然的に非変以外の変位を指定できないのですが、実際に非変な総称型しか使えないとなると総称型の柔軟性が大きく損なわれてしまいます。そこで、Java言語で最後の切り札として採用されたのが「ワイルドカード型」です。ワイルドカード型と呼ばれますが、これは普通の型とは大きく異なる概念です。*3ワイルドカード型は総称型パラメーターの値として<>の中でしか使うことができません。だから、以下のような記述は不正です。

  ? a; // コンパイルエラー
  ? extends B b; // コンパイルエラー
境界のないワイルドカード

まず、総称型変数に「?」を用いることができます。これは型に何が入っているか不明という意味になります。この定義により、?でパラメータ化された型は任意の型でパラメーター化された型のスーパータイプとなります。しかし、何の型が入っているかが不明なため、戻り値はObject型としかみなすことができません。また、パラメーターとして一切の型の値を渡すことができません。以下の例を見てください。

MyBean<String> strValue = new MyBean<String>();
MyBean<Integer> intValue = new MyBean<Integer>();

MyBean<?> list;
list = strValue; // OK
list = intValue; // OK

Object value = list.getValue(); // ?が何のか不明なためObject型以外には代入できない
list.setValue("test"); // コンパイルエラー(?が何の型か不明なため)
list.setValue(1); // コンパイルエラー(?が何の型か不明なため)
上限型つきのワイルドカード

「? extends Number」のようにワイルドカードの型に上限を設定することができます。この場合、?はNumberのサブタイプであることが保障されるため、以下の動作となります。

MyBean<String> strValue = new MyBean<String>();
MyBean<Integer> intValue = new MyBean<Integer>();

MyBean<? extends Number> list;
list = strValue; // コンパイルエラー
list = intValue; // OK(共変のように振舞う)

Number value = list.getValue(); // 値を取得する際にはMyBean<Number>のように振舞う
list.setValue("test"); // コンパイルエラー(?がどのサブタイプか不明なため)
list.setValue(1); // コンパイルエラー(?がどのサブタイプか不明なため)

このように上限つきのワイルドカード型は局所的には共変な型変数のように振舞うことがわかります。メソッドから値を戻すことはできるが、メソッドに値を渡すことができない(ただし、nullは例外)という共変型変数の制約も同時に満たされています。

下限型つきのワイルドカード

「? super Integer」のようにワイルドカードの型に下限を設定することができます。この場合、?はIntegerのスーパータイプであることが保障されるため、以下のようになります。

MyBean<String> strValue = new MyBean<String>();
MyBean<Number> numberValue = new MyBean<Number>();

MyBean<? super Integer> list;
list = strValue; // コンパイルエラー
list = numberValue; // OK(反変のように振舞う)

Object value = list.getValue(); // 値を取得する際にはMyBean<?>のように振舞う
list.setValue("test"); // コンパイルエラー(?がString型になることはないため)
list.setValue(1); // OK(値の設定側ではMyBean<Integer>のように振舞う)

このように下限つきのワイルドカード型は局所的には反変な型変数のように振舞うことがわかります。メソッドに値を渡す場合はもともと型変数のように振る舞います。Object型で値を返すことが可能ですが、本質的には反変型変数の制約も満たされています。

ワイルドカード型の性質についてのまとめ

以上、ワイルドカード型についてまとめると以下のようになります。

ワイルドカード
型種別
書式 タイプの親子関係 値の設定時の振る舞い 値の取得時の振る舞い
境界なし 総称型<?> すべての総称型の親クラス エラー(null以外) Object型として取得
上限つき 総称型<? extends 上限型> 総称型<上限型>
のサブタイプ、
上限型について共変性
エラー(null以外) 総称型<上限型>と等価、
上限型として取得
下限つき 総称型<? super 下限型> 総称型<下限型>
のスーパータイプ、
下限型について反変性
総称型<下限型>
と実質的に等価
総称型<?>と等価

スマホ1台で稼げるバイト

このように、Java言語では過去バージョンとの互換性の制約から総称型変数そのものに変位指定することはできませんが、総称型を利用する側でワイルドカード型を適切に使い分けることで本質的には呼び出し側で個別に変位指定をしていると考えることができます。Javaワイルドカード型を正しく使いこなすには表面上の意味だけでなく、上記の表にまとめたような隠された意味を理解することが大切だと思います。

毎回使う側で変位を意識しなくてはならないため、DRYの法則から考えるとあまり良くないとも言えますが、互換性を犠牲にしないという制約の中で、非常によく考えられていると感心させられます。また、個別に変位が指定できることはある意味では設計の柔軟性にもつながります。ちなみに、このようなワイルドカード型を総称型の利用時に使える言語は、私の知っている言語の中ではJavaが唯一のものです。

体育会出身者の為の転職エージェント【スポナビキャリア】

GetとPutの法則(PECS*4の法則)

このように、総称型のパラメーターとしてワイルドカード型を利用することで、局所的に共変性や反変性を持たせることができ柔軟性を高めることができます。しかし、型安全性を考えると前節でまとめたように共変的な「? extends 上限型」のワイルドカード戻り値として値を取得する側で、反変的な「? super 下限型」のワイルドカードは値の引渡す側で利用すると有用なことがわかります。つまり、以下の法則が成り立ちます。

したがって、コレクションの要素値をコピーするメソッドは以下のように定義するのが正解です。

public static <E> void copy(List<? super E> dst, List<? extends E> src) {
	for (int i = 0; i < src.size(); i++) {
		dst.set(i, src.get(i));
	}
}

もし、ワイルドカードがまったく使われていなければ、同一の型パラメーターを持つコレクション同士でしかコピーができません。以上のようにワイルドカードを使えば、srcのコレクションの要素型がdstのコレクションのサブタイプであるような場合も含めてコピーすることが可能になり、柔軟性がより高くなっています。

なお、GetとPutの法則を適用する際には、そのメソッドパラメーターに対して呼び出しているインターフェースに着目するのもコツです。また、わかりやすい例として、そのパラメーターの型の満たすインターフェースがvoid型なら必ずsuper、あるいは引数なしであれば、必ずextendsになるということもできます。たとえば、Comparator<T>は以下のように定義されています。

public interface Comparable<T> {
    public int compareTo(T o);
}

だから、ワイルドカードを使う場合常にsuperの方になります。実際、Collections.sort()メソッドは以下のシグネチャになっています。

 public static <T> void sort(List<T> list, Comparator<? super T> c) { ...

逆に、Iterator<E>は以下のように定義されています。

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

だからワイルドカードを使うとしたらextendsの方になります。

エンジニアのためのQ&Aサイト【teratail】

境界なしワイルドカード型の使いどころ

上限も下限も設定されていないList<?>のような型は、どういう目的で使えばよいのでしょうか?

  • 値の設定ができない
  • 値を取り出す際にはraw型と同じでObject型としてしか取り出せない。

といった非常に大きな制約があります。しかし、型消去を前提としたJavaの総称型の実装においては以下の性質から結構重要な役割もあります。

  • すべての総称型のスーパータイプとなっている。(非チェック警告つきでダウンキャストできる)
  • 具象化可能型であり、raw型と互換性がある。

つまり、レガシーライブラリーを総称化する際などにうまく型変数が推論できないような場合にとりあえず既存のraw型を境界なしワイルドカード型で置きかえることができます。ただし、raw型と違い、警告なしの使い方では型安全性が保障されていることが大きな違いです。実際には有用な操作を行う場合には特定の型パラメーターを持った型にダウンキャストして使うことが多いのですが、前者の性質から任意の型にキャスト可能です。もちろん、この場合キャストの安全性はチェックされない旨警告が出ます。(あとは自己責任で型安全性に注意してくださいという意味)

さらに、境界なしのワイルドカードは、次節で説明するワイルドカードキャプチャで利用できます。

ワイルドカードキャプチャについて

ワイルドカードキャプチャと呼ばれる特別ルールについて説明します。GetとPutの法則のように「? extends 上限型」は読み取り専用、「? super 下限型」は書き込み専用の場所で使うのが原則です。しかし、以下のコードはコンパイルが通ります。

public static void reverse(List<?> list) { 
	reverseImpl(list);
}

private static <E> void reverseImpl(List<E> list) { 
	List<E> temp = new ArrayList<E>();
	for (int i = 0; i < list.size(); i++) {
		list.set(i, temp.get(list.size() - 1  - i));
	}
}

ここで、最初のメソッドのlistの型パラメータワイルドカードになっています。これを2番目のメソッドに直接バインドしたと考えると概念的には以下のようになるのですが、これは本来はコンパイルの通らないコードです。既に説明したように境界のないワイルドカードを持つ総称型のインスタンスには値が設定できず、値もObject型としてしか取り出せません。

private static void reverseImpl(List<?> list) { 
	List<?> temp = new ArrayList<?>();
	for (int i = 0; i < list.size(); i++) {
		list.set(i, temp.get(list.size() - 1  - i));
	}
}

しかし、よくよく考えてみるとワイルドカードの「?」にどの型がバインドされても同じ型のListの値を読み書きしているだけなので、結局以上のロジックは型安全であることが保障されます。このような場合はワイルドカードが特別な意味を持ち、普通の型のように処理されます。これはワイルドカードキャプチャと呼ばれています。

java.util.Collectionsのメソッドはこのワイルドカードキャプチャが多用されています。こうすることにより、publicなAPIの見栄えを多少簡単にすることができます。

(ただし、個人的にはワイルドカードキャプチャのテクニックを積極的使用したことはありません。)

<T extends 型>と<? extends 型>を混同してはいけない

なお、Javaでは構文がお互いによく似ているためよく混同されがちなのですが、総称型変数を定義する際に型変数に対する上限型境界を与えることが可能です。しかし、これはワイルドカードの型境界とはまったく別の概念です。(C#ではwhere、Scalaでは<:を使って宣言できる。構文上混同はされにくい。)

<T extends 型>のような指定は総称型変数Tに指定可能な型に制約を与えるものであり、変位とは関係ありません。また、ワイルドカード型とは以下のような違いもあります。

  • ワイルドカードと違い<T super 型>のようには宣言できません。
  • &演算子を用いて複数の上限を設定できる。<T extends Serializable & Comparable>のように宣言できます。

ワイルドカード型を指定できそうで、できない場所

最後に、ワイルドカード型を使う上でコンパイルエラーとなる制約について説明します。ワイルドカード型を型パラメーターに持つクラスの生成に関しては仕様上以下の制約があります。

以下の例を見てください。

List<?> list = new ArrayList<?>(); // コンパイルエラー「ArrayList<?>のインスタンスを生成できません。」
List<? extends Number> list2 = new ArrayList<? extends Number>(); // コンパイルエラー「ArrayList<? extends Number>のインスタンスを生成できません。」
List<List<?>> list3 = new ArrayList<List<?>>(); // OK(ワイルドカードがトップレベルでない)
class SomeUtils {

	public static <T> void someGenericMethod(T param) {
		// ...
	}
}

public class Temp {
	public static void main(String[] args) throws Exception {
	    List<?> list = new ArrayList<Number>();

	    SomeUtils.<?>someMethod(list); // エラー「ワイルドカードはこのロケーションでは許可されていません。」
	    SomeUtils.<List<?>>someGenericMethod(list); // OK(ワイルドカードがトップレベルでない)
	}
}
class Parent<T> {}

class Child extends Parent<?> { // エラー型 Child は Parent<?> を拡張または実装できません。スーパータイプはワイルドカードを指定できません
}

class Child2 extends Parent<List<?>> { // OK(ワイルドカードがトップレベルでない)
}

型消去されるのだから、原理的にはワイルドカードが含まれていてもバイトコードに変換できそうなのですが、トップレベルにワイルドカードが含まれる型というのは概念的に一つの型に対応しないため、実体のクラスを指定してnewやメソッド呼び出しをするというのはおかしいということだと思います。インターフェースに対して

new List();

と書けないのと同じ理由と思われます。

 

 

業界TOPクラスのアルバイト情報サイト【ジョブセンス】 【口コミお部屋探しシステム】 フリーランス向け!高額×エンドユーザー直案件なら、【エンジニアファクトリー】 【完全無料プログラミング研修&就活塾】 車を売るなら「車選び.com」 ココナラ

日本最大級のアルバイト求人サイト【アルバイトEX】

ここでしか読めないWEBマンガがある【電子書籍ランキング.com】