S-JIS[2007-05-02/2014-07-05] 変更履歴

総称型(Generic type)

Java総称型(ジェネリクス)は、C++のテンプレート(template)と見た目はそっくりの機能。JDK1.5で導入された。


総称型の概要

JDK1.4までObject型で何でも放り込んでいたListやMap等のクラス・インターフェース何の型でも指定できたClassクラスに対し、使用する型を限定するのに使える。

総称型の利点は、

キャストによる型チェックが実行時に結局行われると思われるので、その分の実行効率が良くなるわけではない点が残念。


クラスの総称型

総称型のクラスを定義するには、以下の例のようにする。(インターフェースも同様)

public class クラス名<仮型引数 ,仮型引数…> {
	private 仮型引数 フィールド;	//メンバー変数定義

	public void セッター(仮型引数 val) {
		this.フィールド = val;
	}

	public 仮型引数 ゲッター() {
		return this.フィールド;
	}
}

仮型引数”には、普通は「T」とか「E」とか、大文字一文字を使う。(TypeとかElementとか、その型の使用目的や意味を表す名称の先頭一文字)
(文法的には一文字である必要はないが、慣例として一文字にする)
そしてTやEを、クラス名として使用できる箇所に書いていく。

public class GenericSample<T> {
	private T value;

	public void setValue(T val) {
		value = val;
	}

	public T getValue() {
		return value;
	}
}

総称型で定義されたクラスを使うには、以下のように、型引数に具体的なクラスを指定する。[2007-06-12]
(→JDK1.7のダイアモンド演算子

	GenericSample<String>  gss = new GenericSample<String>();
	gss.setValue("abc");
	String s = gss.getValue();

	GenericSample<Integer> gsi = new GenericSample<Integer>();
	gsi.setValue(Integer.valueOf(123));
	Integer i = gsi.getValue();
	gss.setValue(Integer.valueOf(123));	//×型が違うので、コンパイルエラーとなる


↓「new GenericSample<String>()」で作った場合、意味合いとしては以下のようなクラスをインスタンス化したことになる。

public class GenericSampleString {
	private String value;

	public void setValue(String val) {
		value = val;
	}

	public String getValue() {
		return value;
	}
}

C++の場合は、実際に上記のような定義が暗黙に作られる。(なのでC++で色んな種類のテンプレートを実体化させると、作られる実行ファイルが膨大な大きさになる)

Javaの場合は、実際には型引数が使われている部分Object型にしたクラスが1つコンパイルされるだけ。

public class GenericSample {
	private Object value;

	public void setValue(Object val) {
		value = val;
	}

	public Object getValue() {
		return value;
	}
}

で、インスタンス化して使う方のコードに指定された型引数でキャストしていくことで整合性をとっている。

	GenericSample<String> sample = new GenericSample<String>();
	sample.setValue("abc");
	String str = sample.getValue();

↓(コンパイルすると、以下のようになる)

	GenericSample sample = new GenericSample();
	sample.setValue("abc");
	String str = (String)sample.getValue();

Throwableの派生クラスは総称型にすることは出来ない(Java言語仕様第3版8.1.2章『ジェネリック・クラスと型パラメータ』)。[2008-09-13]

catch節(JavaVM)の方が<T>の付いた形式に対応してないからだそうだ。確かに。
仮に出来たとすると、こんな感じ↓

	try {
		〜
	} catch (MyException<String> e) {
		System.out.println("string");

	} catch (MyException<Integer> e) {
		System.out.println("integer");
	}

コンパイルすると型引数の情報は消えるから、同じMyExceptionを2つ指定する状態になってしまう。


総称型の配列を作ることも出来ない。[2010-01-30]

総称型の配列を宣言することは出来るのだが、インスタンスを生成する式が書けない。
総称型を使わない配列を作ることは出来るが、総称型を使った変数に代入する際に警告が出てしまう。

//×	List<String>[] listArray = new List<String>[4];
	@SuppressWarnings("unchecked")
	List<String>[] listArray = new List[4];	//○
//△	List[] listArray = new List[4];	//総称型を使えという警告が出る
	List<?>[] listArray = new List<?>[4];	//○

	List<String> slist = new ArrayList<String>();
	listArray[0] = slist;			//○

//×	List<String> list = listArray[0];			//明示的なキャストが必要
	List<String> list = (List<String>)listArray[0];	//未検査キャストの警告が出る

ダイアモンド演算子

JDK1.7で、総称型を使ったクラスをインスタンス化する際に型引数を省略できるようになった。[2012-01-14]
通称ではダイアモンドオペレーターと言うらしい。

	List<String> list = new ArrayList<String>();
↓
	List<String> list = new ArrayList<>();
	List<? extends String> list2 = new ArrayList<>();
	Map<String, Integer> map = new HashMap<>();

ただし、サポートされているのは限定的であり、構文上明白な場合でないと使用できない。

void method(List<String> list) { 〜 }

	method(new ArrayList<>());	//メソッド呼び出しは駄目
	List<String> list3 = new ArrayList<>() { };	//匿名クラスは駄目

メソッドの総称型

メソッドのみに総称型を適用した定義を行うことも出来る。[2007-06-12]
戻り値の型の直前に「<仮型引数, 仮型引数…>」を置くだけ。後はクラスの場合と同様。

	public static <T> T notNull(T arg1, T arg2) {
		T ret;
		if (arg1 != null) {
			ret = arg1;
		} else {
			ret = arg2;
		}
		return ret;
	}

このメソッドを呼び出す側は、クラスの場合とは異なり、明示的に型引数を指定する必要は無い。
指定した引数の値に応じて、自動的に型が判断される。

	String s = notNull("abc", "def");		//型引数はString
	Long   l = notNull(new Long(1), new Long(2));	//型引数はLong
	int    i = notNull(1, 2);			//型引数はInteger(intのラッパー)
	int    j = notNull(1, null);		//型引数はInteger(intのラッパー)
	String t = notNull(null, null);		//型引数はString
	notNull(null, null);			//型引数はObject

“定義の中で同じ型引数を使っている箇所”に対し、使う際に“異なるクラスのインスタンス”を指定すると、その中で共通なスーパークラスが指定されたことになる。
どのクラスもObjectを継承しているので、最悪でも(最悪だと)Objectクラスになる。

	notNull((int)1, (long)2);		//型引数はNumber(intのラッパーIntegerとlongのラッパーLongは、どちらもNumberからの派生)
	notNull(Long.valueOf(1), "abc");	//型引数はObject

メソッドの仮型引数は、別の総称型の型引数にもマッチしてくれる。[2008-08-21]

	static <T> void print(List<T> list) {
		for (T e : list) {
			System.out.println(e);
		}
	}
		List<String> ls = new ArrayList<String>();
		ls.add("abc");
		print(ls);	//TはString

		List<Integer> li = new ArrayList<Integer>();
		li.add(123);
		print(li);	//TはInteger

メソッドの総称型でも、明示的に型引数を指定することが出来る。[2008-07-11]

/** 明示的に型引数を指定する為のサンプル */
class ExplicitSample {

	//静的メソッド
	static <T> T staticMethod(T obj) {
		return obj;
	}

	//インスタンスメソッド
	<T> T instanceMethod(T obj) {
		return obj;
	}
}
		ExplicitSample.<String>staticMethod("abc");
		ExplicitSample.<Number>staticMethod(Long.valueOf(123));

		ExplicitSample es = new ExplicitSample();
		es.<String>instanceMethod("zzz");
		es.<Number>instanceMethod(Integer.valueOf(123));

staticメソッドの場合は、メソッドの前に「クラス名.<型>」を書く。自分自身のクラス内であっても、クラス名は省略できない。
インスタンスメソッドの場合は、メソッドの前に「変数名.<型>」を書く。自分自身(this)だったら、「this.<型>」とする(thisは省略できない)。

明示的な型指定の使用例


コンストラクターの総称型

メソッドのみの総称型と同様に、コンストラクターのみに総称型を適用した定義を行うことも出来る。[2008-09-14]

class Constructor {
	public <T> Constructor(T arg) {
		System.out.println(arg);
	}

	public Constructor(int n) {
		this(Integer.valueOf(n));	//(別コンストラクターの呼び出し)自動的に型も判断してくれる
	}

	public Constructor() {
		<String>this(null);	//(別コンストラクターの呼び出し)明示的な型指定も可能
	}
}
class Constructor2 extends Constructor {
	public Constructor2(Number arg) {
		super(arg);		//(親コンストラクターの呼び出し)Numberとして呼び出す
	}
	public Constructor2(String arg) {
		super(arg);		//(親コンストラクターの呼び出し)Stringとして呼び出す
	}

	public Constructor2() {
		<String>super(null);	//(親コンストラクターの呼び出し)明示的な型指定も可能
	}
}
		Constructor ca = new Constructor("abc");
		Constructor cb = new<String> Constructor(null);	//明示的な型指定も可能

型引数で出来ないこと

new

型引数を使った(newによる)インスタンス生成は出来ない。[2008-07-11]

class Sample<T> {
	public void class_newT() {
		T obj = new T();	//「型Tのインスタンスを生成できません」というコンパイルエラー
	}

	public <S> void method_newS() {
		S obj = new S();	//「型Sのインスタンスを生成できません」というコンパイルエラー
	}
}

型引数の実際のクラスが決まるのは実行時だからなぁ…。


instanceof

型引数を使ったinstanceofもコンパイルエラーになる。[2008-12-08]

		if (obj instanceof T) {
			〜
		}

.class

型引数を使ったメソッド呼び出しやフィールドアクセス(つまりstaticメソッド・staticフィールド)は出来ない。[2008-07-11]
ってTをObjectクラス扱いしたとしても、staticなメソッドは存在してないか(汗)

class Sample<T> {
	public void dump() {
		Class c = T.class;	//「型パラメーターTのクラス・リテラルが正しくありません」というコンパイルエラー
	}
}

ジェネリクスを使ったソースをコンパイルすると、型引数の部分がObjectに置き換わる。[2008-08-10]
つまり仮に上記のクラスがコンパイルできたとすると、

class Sample {
	public void dump() {
		Class c = Object.class;
	}
}

となる。そんな情報は誰も欲しくないだろう(苦笑) だからコンパイルエラーになってくれるのは親切なわけだ。


extendsを使って限定した場合でも、そのクラスの呼び出しにしかならない。[2008-09-14]

class Static2<T extends Calendar> {
	void method() {
		Calendar cal = T.getInstance();
		System.out.println(cal.getClass());
	}
}

この例の場合、Tにどんなカレンダークラスを指定したとしても、呼び出されるのは常にCalendar#getInstance()。

staticフィールドでも同様に、extendsで限定したクラスへのフィールドアクセスにしかならない(そのクラスへのフィールドアクセスは出来る)。

そしてこの場合でも、やはりT.classはコンパイルエラーになる。


static

staticなメンバー(フィールド・メソッド・内部クラス)でクラスの型引数を使うことは出来ない。[2008-07-11/2008-07-14]

class Sample<T> {
	private static T value;		//「非static型Tをstatic参照できません」というコンパイルエラー

	public static void set(T obj) {	//「非static型Tをstatic参照できません」というコンパイルエラー
		value = obj;
	}

	public static T get() {		//「非static型Tをstatic参照できません」というコンパイルエラー
		return value;
	}

	static class Foo {
		private T zzz;		//「非static型Tをstatic参照できません」というコンパイルエラー
	}
}

もし出来たとすれば、以下のように使えるのに…。

		Sample<String>.set("abc");
		String s = Sample<String>.get();

Sample内の実体はObjectにしておいて、使う側はコンパイル時に上手くキャストしてくれれば問題ないよね。
と言いたいところだが、複数の型を指定した場合…

		Sample<String>.set("abc");
		Sample<Integer>.set(123);

Sampleクラスのstaticな領域は、JavaVM内に1つしか無い。
つまり、StringとIntegerを別々に保持することは出来ないわけだ。


型引数でないstaticフィールドにも、クラスの型引数を指定してアクセスすることは出来ない。[2008-09-13]

class Static<T> {
	public static int value;
}

//×	int n = Static<String>.value;
	int n = Static.value;

イレイジャ(イレージャ)

総称型として定義されているクラスから型パラメータを除去したクラスを「型のイレイジャ(型消去:type erasure)」と呼ぶ。[2008-09-13]

つまり「List<T>」に対する「List」、「Map<K, V>」に対する「Map」が“イレイジャ”。


未加工型(raw型)

総称型として定義されているクラスであっても、そのクラスのイレイジャを使って使用することが出来る。[2008-09-13]
(イレイジャは「型引数を除去したクラス名」であって、除去されたクラスを実際に使うと未加工型(raw type)という位置づけになるっぽい)

つまり、例えばList<T>やArrayList<T>(総称型として定義されているクラス)を使う場合は、以下のように書くのが正しい。

	List<String> list1 = new ArrayList<String>();
	List<?> list2 = foo();
	List<? extends Number> list3 = null;

しかし、以下のようにイレイジャを使って書くことも出来る。

	List list1 = new ArrayList<String>();
	List<String> list1 = new ArrayList();	→コンパイル時の警告
	List list1 = new ArrayList();
	List list2 = foo();
	List list3 = null;

これは従来のJDK1.4までの書き方と同様。
すなわち、互換性の為に許されているのであって、将来バージョンでは禁止になる可能性があるJava言語仕様第3版4.8章『未加工型』)というくらいのシロモノらしい。


総称型は共変でない

総称型は共変(covariant)でない。[2008-05-21]
共変というのは、継承関係のある親クラスに変換できることっぽい。

	List<Integer> list = new ArrayList<Integer>();
	List<Number> nlist = list;	//コンパイルエラー

IntegerはNumberのサブクラスだが、List<Integer>からList<Number>へ代入することは出来ない。
配列では出来るが、これは危険行為なので すべきではない)

共変戻り値も使えない


総称型の限定(制約付きパラメーター)

仮型引数を定義する際にextendsを使うことにより、「どのクラス(の派生クラス)であるか」を指定して 使える型を限定する(制約を付ける)ことが出来る。

クラスの場合:

//Numberを継承したクラスのみに限定したクラス
class SampleExtends<T extends Number> {
	public void dumpInt(T arg) {
		//TはNumberの派生クラスであることが保証されるので、Number#intValue()が使える。
		System.out.println(arg.intValue());
	}
}
		SampleExtends<Long> l = new SampleExtends<Long>();
		l.dumpInt(new Long(123));
		new SampleExtends<String>();	//×StringNumberの派生クラスではないので、コンパイルエラー

メソッドの場合:

	//Numberを継承したクラスのみに限定したメソッド
	protected <N extends Number> boolean less(N arg1, N arg2) {
		return arg1.intValue() < arg2.intValue();
	}
		if (less(1, 2)) { 〜 }

複数のクラス・インターフェースを継承・実装していることを条件にする場合は、「&」でつないで列挙する。[2008-05-21]

class C1 { 〜 }
interface I2 { 〜 }
interface I3 { 〜 }

class SampleExtends<T extends C1 & I2 & I3> {
	public static void test(T arg) {
		C1 i1 = arg;	//argのクラスであるTはC1から派生しているのが保証されるので、C1に代入可能
		I2 i2 = arg;	//argのクラスであるTはI2から派生しているのが保証されるので、I2に代入可能
		I3 i3 = arg;	//argのクラスであるTはI3から派生しているのが保証されるので、I3に代入可能
		〜
	}
}
class SampleII extends C1 implements I2, I3 { 〜 }

		SampleExtends<SampleII> s = new SampleExtends<SampleII>();
		SampleII sii = new SampleII();
		se.test(sii);

ただし、クラスは1回だけしか指定できない(0個、すなわち全てインターフェースというのは可)。
また、クラスはextendsの直後にしか指定できない(2番目以降でクラスを指定することは出来ない)。

なんで<T extends C implements I1, I2>ってしないのかな…と思ったけど、仮型引数自身もカンマ区切りで複数並べられるから、implementsのカンマと区別つかなくなっちゃうねー。
×<T extends C1 implements I1, I2 , S implements I3, I4>
○<T extends C1 & I1 & I2, S extends I3 & I4>


不定を意味する型引数(ワイルドカード)

総称型で定義されているクラスやメソッドを使う際に、実行してみるまで型が限定されない場合がある。
この場合は、型引数に「?」を指定する。“ワイルドカード(境界ワイルドカード:unbounded wildcard)”と呼ぶらしい。[/2008-05-21]

	List<?> list = null;
	Map<String, ?> map = null; //キーがStringということだけ決まっている
	Class<?> c = Class.forName("Sample");
	//Listを返すのは決まっているが、何の型のリストか実行するまで分からない場合の例
	public List<?> createList(boolean s) {
		if (s) {
			return new ArrayList<String>();
		} else {
			return new ArrayList<Integer>();
		}
	}

これにもextendsを使うことが出来る。“上限境界ワイルドカード(upper bounded wildcard)”と呼ぶらしい。[/2008-05-21]
(指定されたクラス・インターフェース自身か、そのサブクラスであることを示す。なお、「&」は使えない)

	Class<? extends Number> c = Integer.class;
	Number n = c.newInstance();
	public List<? extends Number> createNumberList(boolean f) {
		if (f) {
			return new ArrayList<Float>();
		} else {
			return new ArrayList<Double>();
		}
	}

また、superを使うことも出来る。“下限境界ワイルドカード(lower bounded wildcard)”と呼ぶらしい。[2008-05-21]
(指定されたクラス・インターフェース自身か、その親クラスであることを示す)

class S1 {}
class S2 extends S1 {}
class S3 extends S2 {}	//S1←S2←S3

class Temp<T> {}

public class Main {
	public static void test(Temp<? super S2> obj) { 〜 }

	public static void main(String[] args) {
		test( new Temp<S1>() );	//OK
		test( new Temp<S2>() );	//OK
		test( new Temp<S3>() );	//コンパイルエラー
	}
}

参考: WisdomSoftさんのワイルドカード


ワイルドカード「?」は、“(指定された範囲の)いずれかのクラスである”ことを示す。すなわち、具体的なクラスは不明。
例えば「? extends Number」はIntegerかもしれないしLongかもしれない。
だから「?」付きの総称型には値を入れることが出来ない。

	List<? extends Number> list = new ArrayList<Integer>();
	list.add(new Integer(123));	//IntegerはNumberのサブクラスなのにコンパイルエラー
	list.add(new Long(123));	//LongもNumberのサブクラスだけどコンパイルエラー

「extends Number」なので「listの要素はNumberのサブクラスである」ことだけはコンパイル時に分かるが、その実体がIntegerなのかLongなのかは 「?」なので分からない。
だからIntegerインスタンスを入れることは出来ない。
もし「Numberのサブクラスだから」という理由で入れられるなら、実体がArrayList<Integer>であってもLongが入れられることになってしまう。
それはタイプセーフ(型安全)でない。


<C>と<? extends C>の違い

<C>と<? extends C>には、以下のような違いがある。[2008-12-25]

(↓クラスC1(class C1{})とそれを継承したサブクラスC2(class C2 extends C1{})があるとする)

	public List<C1> returnList() {
		return new ArrayList<C1>();		//OK
//		return new ArrayList<C2>();		//コンパイルエラー(総称型は共変ではないので、親クラスへ代入できない)
	}

	public List<? extends C1> returnListQ() {
		return new ArrayList<C1>();		//OK
//		return new ArrayList<C2>();		//OK(<? extends C1>はC1のサブクラスを受け付ける)
	}
		List<C1> list1 = returnList();		//OK
		List<? extends C1> list2 = returnList();	//OK(<C>から<?>へは代入可能)

//		List<C1> list3 = returnListQ();		//コンパイルエラー(<?>は具体的なクラスが不明なので、具体的なクラスへ代入できない)
		List<? extends C1> list4 = returnListQ();	//OK
//class List<E> { public E get(int n){〜} }

		List<C1> list1 = 〜;
		C1 obj1 = list1.get(0);		//OK

		List<? extends C1> list2 = 〜;
		C1 obj2 = list2.get(0);		//OK(C1から派生している事は分かっているので、C1には代入できる)

superの使用例

「? super」は、例えばCollections#sort()で使われている。[2008-05-22]

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

どうしてこうなるのか?
Comparableを実装したクラス(下記のCom)をListに入れて、そのListから最大値を返す(sort()と似た)メソッドを考えてみる。

/** 処理対象クラス */
class Com implements Comparable<Com> {

	protected int value;

	public Com(int n) {
		this.value = n;
	}

	public int compareTo(Com o) {	//Comparableインターフェースで宣言されたメソッドの実装
		int o_value = (o == null) ? 0 : o.value;
		return this.value - o_value;
	}

	@Override
	public String toString() {
		return Integer.toString(this.value);
	}
}

1.Comのみに対応したメソッド

このComが入ったリスト(List<Com>)から最大値を探すメソッドは、以下のようになる。

	public static Com max1(List<Com> list) {
		Com max = null;
		for (Com c : list) {
			if (max == null) {
				max = c;
			} else {
				if (max.compareTo(c) < 0) {
					max = c;
				}
			}
		}
		return max;
	}
		List<Com> clist = new ArrayList<Com>();
		clist.add(new Com(22));
		clist.add(new Com(33));
		clist.add(new Com(11));

		Com c = max1(clist);
		System.out.println(c);

これには別段不思議なところは無い。


2.Comのサブクラスに対応したメソッド

次に、このComを継承したクラスを作ってみる。

class Sub extends Com {

	public Sub(int n) {
		super(n);
	}
}
		List<Sub> slist = new ArrayList<Sub>();
		slist.add(new Sub(22));
		slist.add(new Sub(33));
		slist.add(new Sub(11));

		Sub s = (Sub) max1(slist);	←コンパイルエラー!
		System.out.println(s);

すると、List<Com>を引数とするmax1()メソッドを呼ぶ箇所でコンパイルエラーになった。
なぜなら、ジェネリクスは共変でないので、List<Sub>からList<Com>へ変換(代入)することは出来ないから。
(仮に出来ても、戻り値はComなのでSubにキャストしてやる必要があるし)

Listでなく配列で同様のメソッドを作ると、(配列は共変なので)Sub[]でもコンパイルエラーにならない。

	public static Com max1(Com[] array) {
		Com max = null;
		for (Com c : array) {
			if (max == null) {
				max = c;
			} else {
				if (max.compareTo(c) < 0) {
					max = c;
				}
			}
		}
		return max;
	}
		Com[] carr = { new Com(11), new Com(33), new Com(22) };
		Sub[] sarr = { new Sub(11), new Sub(33), new Sub(22) };
		Com c =      max1(carr);
		Sub s = (Sub)max1(sarr);


そこでmax1()を改造し、Com限定でなく、型引数Tを受け付けるようにしてみる。

	public static <T> T max2(List<T> list) {
		T max = null;
		for (T c : list) {
			if (max == null) {
				max = c;
			} else {
				if (max.compareTo(c) < 0) {		←コンパイルエラー!
					max = c;
				}
			}
		}
		return max;
	}

すると、compareTo()を呼び出す箇所でコンパイルエラーとなった。
型引数Tには「オブジェクトである」という情報(前提条件:全てのクラスはObjectを継承する)しか無い為、Objectのメソッドしか呼べない。
ObjectにはcompareTo()は存在しないので、コンパイルエラーになるのは当然だ…。

この場合、TがComparableを実装(継承)していることを示してやればよいので、extendsの出番となる。

	public static <T extends Comparable> T max2(List<T> list) {
		T max = null;
		for (T c : list) {
			if (max == null) {
				max = c;
			} else {
				if (max.compareTo(c) < 0) {		←コンパイル警告
					max = c;
				}
			}
		}
		return max;
	}

エラーは無くなったが、警告になった。
Comparableインターフェースは「Comparable<C>」という型引数を持つのに それを指定していないから。
(ただし、List<Com>を渡してもList<Sub>を渡しても、実行は出来る。)

警告が出ているだけでも気持ち悪いが、しかし/しかも、以下のような変なクラスを渡すと問題が起きる

class Hen implements Comparable<Integer> {

	protected int value;

	public Hen(int n) {
		this.value = n;
	}

	public int compareTo(Integer o) {
		int o_value = (o == null) ? 0 : o.intValue();
		return this.value - o_value;
	}

	@Override
	public String toString() {
		return Integer.toString(this.value);
	}
}
		List<Hen> hlist = new ArrayList<Hen>();
		hlist.add(new Hen(22));
		hlist.add(new Hen(33));
		hlist.add(new Hen(11));
		Hen h = max2(hlist);	//コンパイルエラーにはならないが…
		System.out.println(h);

max2()の呼び出しではコンパイルエラーにも警告にもならない。HenComparableを実装しているのだから、コンパイルが通るのは正しい…。
しかし、実行するとmax2()内のcompareTo()呼び出しで例外が発生する。

Exception in thread "main" java.lang.ClassCastException: Hen cannot be cast to java.lang.Integer

max2()内のcompareTo()は呼出元も比較先も同じクラスであることを想定している(同じ仮型引数Tで宣言している)のに、HenのComparableの実装はそうなっていないから。

こういう変な(max2()メソッドの想定外の)クラスこそ、コンパイル時点で除外したい(エラーにしたい)ものだ。


3.Comparableの仮型引数を指定したメソッド

この問題を解決するには、Comparableの型引数をきちんと指定する必要がある。

ワイルドカードを使って「Comparable<?>」とすると、compareTo()呼び出しで やはりコンパイルエラーになる(型が不定だから)。

	public static <T extends Comparable<?>> T max3(List<T> list) {


「Comparable<T>」とすると、List<Com>は呼び出せるが、List<Sub>が呼び出せない(SubはComparable<Sub>を実装している訳では無いから)。

	public static <T extends Comparable<T>> T max3(List<T> list) {
		Com c = max3(clist);	//OK
		Sub s = max3(slist);	//コンパイルエラーになってしまう


結局のところ、Comを指定した時に「Comparable<Com>」が許容され、Subを指定した時に「Comparable<Com>」が許容されれば問題が解決する。
つまり、Comparable<C>のCには、ComのときはComSubのときは(Subの親クラスである)Comが指定できればよい。
つまり、Comparable<C>のCには、T自身またはTの親クラスが指定できればよい。
それがばっちりsuperの指定に当たる。

	public static <T extends Comparable<? super T>> T max4(List<T> list) {
		T max = null;
		for (T c : list) {
			if (max == null) {
				max = c;
			} else {
				if (max.compareTo(c) < 0) {
					max = c;
				}
			}
		}
		return max;
	}
		Com c = max4(clist);	//OK
		Sub s = max4(slist);	//OK
		Hen h = max4(hlist);	//コンパイルエラーになってくれる

“変なComparableの実装をしたHen”を使った呼び出しも、コンパイル段階でエラーになってくれる。

TがSubのとき、SubはComparable<Com>を実装しているので、ワイルドカード「?」部分はCom。“Tの親クラスであれば良い”という条件に合致しているのでOK。
TがComのとき、ComはComparable<Com>を実装しているので、ワイルドカード「?」部分はCom。“Tの親クラス(自分も含む)”という条件に合致しているのでOK。
TがHenのとき、HenはComparable<Integer>を実装しているので、ワイルドカード「?」部分はInteger。“Tの親クラス”という条件に合致していないのでコンパイルエラーになる。
仮にHenがComparable<Hen>も実装したらどうなるか…と思って試してみたら、同じインターフェースを別の型引数で重複して実装することは出来ないようだ(コンパイルエラーになった)。
複数の型で同一インターフェースを実装できないというのは、それはそれで不便な場面はありそうだけど。
Comparableで言えば、compareTo(int)とかcompareTo(String)とか作りたいと思っても不思議じゃないような。オーバーロードは出来るのでメソッド自体は作れるけど、それをComparableのメソッドだと言えないのが困るとか。…でも、こういう考えは、そもそものComparableの想定(仕様)と違うかも。Comparableは、あくまでそれを実装したクラス自身の比較を意味する気がしてきた。ComparableのJavadocには明記されていない気もするけど…使い道を考えれば、そうでないとおかしい。
そういう意味だと、Comparable<T>のTが「実装したクラス自身である」という制限が出来るといいんだろうけど…そういう文法は無さそうだなぁ。


Collections#sort()を再掲:

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

まさに、今回作ったメソッドと同じ宣言になっている。


getClass()の不思議

Object#getClass()はJDK1.4〜1.6にかけて定義が変化している。[2008-08-10]

1.4	public final Class getClass()
1.5	public final Class<? extends Object> getClass()
1.6	public final Class<?> getClass()

で、JDK1.5でもJDK1.6でも、以下のコンパイルは通る。

	Number n = Integer.valueOf(123);
	Class                   c0 = n.getClass();
	Class<?>                c1 = n.getClass();
	Class<? extends Number> c2 = n.getClass();
	Integer i = Integer.valueOf(123);
	Class<? extends Integer> ci = i.getClass();
	Class<? extends Number>  cn = i.getClass();
	Class<? extends Object>  co = i.getClass();

ところが、これって実は不思議。

自分で同じシグニチャー・戻り型のメソッドを作ってみると…

class Sample {
	public final Class<?> getMyClass() {
		return super.getClass();
	}
}
	Sample s = new Sample();
	Class<? extends Sample> cc = s.getClass();	//OK
	Class<? extends Sample> cm = s.getMyClass();	//コンパイルエラー(<?>から<? extends>に変換できない)

実は、getClass()だけ言語仕様で特別扱いされている。[2008-09-13]
Java言語仕様第3版4.3.2章『クラスObject』)


二重の総称型指定

型引数の指定にさらに総称型のクラスを使った場合、「List<List<String>>」の様に、「>>」が現れることがある。
C++では これがシフト演算子と認識され、コンパイルエラーとなった。「> >」という風に、スペースを入れるのが解決策。
Javaでは問題ない。


動的型保証

ジェネリクスはコンパイル時点での型保証を行うものなので、実行時には型はチェックされない。[2008-12-08]

コレクションで間違った型が入っていると、その型として使用しようとした時点でClassCastExceptionが発生するのみ。

	@SuppressWarnings("unchecked")
	void test() {
		List<String> slist = new ArrayList<String>(); //Stringのリストを作成
		slist.add("abc");

		List list = slist;
		list.add(Integer.valueOf(123));	//String以外のオブジェクトを格納(エラーにならない)

		String s = slist.get(1);	//Stringとして取得→コンパイルエラーにはならないが実行時にClassCastException
		System.out.println(s);
	}

コレクションに関しては、こういう事態にならない為に、java.util.Collectionsクラス“動的な型保証を行うコレクション”を作成するメソッドが用意されている。

	@SuppressWarnings("unchecked")
	void test() {
		List<String> slist = Collections.checkedList(new ArrayList<String>(), String.class);
		slist.add("abc");

		List list = slist;
		list.add(Integer.valueOf(123));	//コンパイルエラーにはならないが実行時にClassCastException

		String s = slist.get(1);
		System.out.println(s);
	}

いずれにしてもコンパイルエラーにはならず、実行時にClassCastExceptionが発生することに違いは無いのだが(苦笑)、発生箇所が違う。
get()の際に例外が発生するのでは、どこかで違う型が代入されたせいだが、どこで代入されたのか分からない(バグの箇所が分からない)。
しかしadd()の際に例外が発生するなら、確実にそこが悪い。

コレクション メソッド 備考 更新日
リスト checkedList(List, Class)    
マップ checkedMap(Map, Class, Class)    
checkedSortedMap(SortedMap, Class, Class)    
セット checkedSet(Set, Class)    
checkedSortedSet(SortedSet, Class)    
checkedNavigableSet(NavigableSet, Class) JDK1.8 2014-07-05
一般 checkedCollection(Collection, Class)    

しかしこれらのchecked系メソッドは、呼び出す前に不正な型が入っていてもチェックしてくれない。

	@SuppressWarnings("unchecked")
	void test() {
		List list = new ArrayList();
		list.add(Integer.valueOf(123));

		List<String> slist = Collections.checkedList(list, String.class);	//エラーになってくれない
		slist.add("abc");

		String s = slist.get(0);	//実行時にClassCastException
		System.out.println(s);
	}

そこで、入っている値が正しいかどうかチェックするメソッドを作ってみた。

	@SuppressWarnings("unchecked")
	public static <E> List<E> castList(List<?> list) {
		for (Object obj : list) {
			if (obj instanceof E) continue;

			throw new ClassCastException("object[" + obj + "][" + obj.getClass() + "] is not element type[" + E + "]");
		}

		return (List<E>)list;
	}

が、型引数Eではinstanceofは使えないし、"["+E+"]"といった文字列結合も出来ない(コンパイルエラーになる)。
(実行時にはEはObjectクラスだから。
 という以前に、「E+""」は「String+""」と同じ意味だから、出来るわけない(苦笑) この場合は「E.class+""」か。でもE.classも無理

なので、比較対照となる(正しい)クラスも引数で渡してやらないといけないようだ。(Collections#checked系メソッドもClassを渡している)

	@SuppressWarnings("unchecked")
	public static <E> List<E> castList(List<?> list, Class<E> type) {
		for (Object obj : list) {
			if (type.isInstance(obj) || obj == null) continue;

			throw new ClassCastException("object[" + obj + "][" + obj.getClass() + "] is not element type[" + type + "]");
		}

		return (List<E>)list;
	}

型パラメーターを実行時に取得する方法

自クラス<T>」のT自クラス内で取得することは基本的には出来ない。[2012-01-14]
コンパイルした時点で型が消去される為、リフレクションでも取得する方法は提供されていない。

しかし、可変長引数を上手く使って型パラメーターのクラスを取得することが出来る。
参考: skirnirさんの型パラメータつきクラスの中から指定された型パラメータを知る方法

class Sample1<T> {
	private Class<T> parameterType;

	public Sample1(T... dummy) {
		@SuppressWarnings("unchecked")
		Class<T> type = (Class<T>) dummy.getClass().getComponentType();

		parameterType = type;
	}

	public Class<T> getParameterType() {
		return parameterType;
	}
}

getComponentType()は、配列の型(クラス)を取得するメソッド。
可変長引数は配列なので、可変長引数の型すなわち型パラメーターのクラスを取得できることになる。

そして、可変長引数なので、呼び出す側は0個の引数として扱う(特に何も記述しない)ことが出来る。

	Sample1 s = new Sample1<String>();
	System.out.println(s.getParameterType());

	Sample1 i = new Sample1<Integer>();
	System.out.println(i.getParameterType());

応用すれば、メソッドのみの型パラメーターでも使える?!

class Sample2 {
	public <T> void method(T arg, T... dummy) {
		@SuppressWarnings("unchecked")
		Class<T> type = (Class<T>) dummy.getClass().getComponentType();

		System.out.println(arg + "\t" + type);
	}
}
	Sample2 s = new Sample2();
	s.method("abc");	//String
	s.method(123);	//Integer

と思ったが、値を直接引数で渡していれば、そのインスタンスのクラスを直接取得すればいいだけだった(爆)

	public <T> void method2(T arg) {
		System.out.println(arg + "\t" + arg.getClass());
	}

うーん、使途を無理矢理考えると…「引数がnullになりうる」ときや「複数の値(具象クラスは別)を引数で渡すが型パラメーターは共通で1つ(method3(T arg1, T arg2))」のときは可変長引数方式にも意味があるか。


まぁ、Javaなら、素直にClass<T>を引数で渡すのが標準的なやり方だと思う^^;

このやり方(コーディング上は呼び出す引数を省略できるが、実際にはコンパイル時点で引数が生成される)は、
ScalaClassTag暗黙の引数として渡す方法(表面上、何も指定する必要が無い)とよく似ている。
Scalaではこの方法はごく一般的。


Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま