Java言語のClassクラスが持つちょっと不思議な性質について
前回のエントリーJava5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる - 達人プログラマーを目指してで、Javaの型システムが総称型の導入によりJava5から大きく拡張されたということを説明しました。ポイントは
- 総称型によりソースコード上は型システムが拡張されている。
- JDK1.4までは型とClassクラスが一対一に対応していた*1がJava5では型消去により対応しなくなった。(対応するのはClassでなくてType)
- Classクラスは具象化可能型と対応する。
ということでした。以前のバージョンの時の感覚でClassクラスと型が一対一に対応していると考えていると勘違いしてしまうところもあります。今回はJava5以降のClassクラスについて、型消去により生じるちょっと不思議な性質について説明させていただきたいと思います。
Java5以降ではClassクラスと型は一対多の関係にある
見かけ上うまいこと工夫されているため、ソースコード上は理解しにくいのですが、このブログで既に何度か説明してきたように、型パラメーターの情報はバイトコード上からは消去されてしまいます。したがって、List
// パラメーターの型によらずにすべて型の消去されたArrayListと同じ。 ArrayList<Integer> intList = new ArrayList<Integer>(); ArrayList<String> strList = new ArrayList<String>(); assert intList.getClass() == strList.getClass(); assert intList.getClass() == ArrayList.class; assert strList.getClass() == ArrayList.class;
このようにコンパイル時の静的な型は違っていても、実行時の型はすべて消去型に等しくなります。
Classの型変数Tにバインドしてよいのは具象化可能型だけ
ArrayList
しかし、Classは実行時の型を表現する特別な役割があるため、型変数には型消去の影響を受けない具象化可能型(か「? extends具象化可能型」の形のワイルドカード型)に限定する必要があります。つまり、Class>などの宣言は文法上は可能なのですが、前節で見たようにList
>のような型の変数を利用すると、以下のような矛盾が起こります。
ArrayList<Integer> intList = new ArrayList<Integer>(); ArrayList<String> strList = new ArrayList<String>(); // このキャストは型安全でないため警告が出るが、警告を意図的に無視 @SuppressWarnings("unchecked") Class<ArrayList<Integer>> intListClass = (Class<ArrayList<Integer>>)intList.getClass(); strList.add("test"); intList = intListClass.cast(strList); Integer value = intList.get(0); // ClassCastException
もともと、Classクラスは実行時の型と対応する前提で設計されているため、TにArrayList
Java言語仕様上は、できるだけClassの型変数Tに不適切な型がバインドされない工夫がされている
前節の例では警告を無視して無理やりClass
Class<ArrayList<Integer>> intListClass = new Class<ArrayList<Integer>>(); //エラー
のような書き方はできません。また、クラスを生成する手段としてクラスリテラルがありますが、この場合も具象化可能型以外*2に対してクラスリテラルを指定できないようになっています。
Class<List> listClass = List.class; // OK Class<List<String>> intListClass = List<String>.class; // エラー
一方、Classクラスのインスタンスを取得する手段としてもう一つ重要なものとして、ObjectクラスのgetClass()メソッドがあります。このメソッドの戻り値の型はObjectクラスのソースコード上はClass<?>と宣言されていますが、JavaDocや言語仕様の4.3.2節で書かれているとおり、コンパイラーの特別ルールでClass<? extends |T|>となることが規定されています。ここで|T|というのは宣言されている静的な型Tの消去型を表します。
実際に実験してみると、以下のコードがコンパイルできることがわかります。
public class Temp<T, S extends Serializable & Comparable<? super T>> { public void test(T t, S s) { String strValue = "test"; List<String> strList = Arrays.asList("test"); Class<? extends String> strClass = strValue.getClass(); Class<? extends List> listClass = strList.getClass();// Class<? extends List<String>>ではない! Class<? extends Object> tClass = t.getClass(); // Class<? extends T>ではない! Class<? extends Serializable> sClass = s.getClass();// Class<? extends S>ではない! } }
ここで、TやSなどの型変数については、上限が設定されていない場合はObjectが、上限が設定されている場合はもっとも左側で指定した上限型が消去型になることに注意してください。コンパイラーの特別ルールによって、宣言しているオブジェクト変数の型に対応したパラメーターがバインドされたClassのインスタンスを得ることができます。しかし、この仕様によって型パラメーターが具象化可能型でない型になることはありません。
もしこの言語仕様がClass<? extends |T|>ではなくClass<? extends T>を返すとしたら、以下のサンプルが警告なしで実行できることになってしまうため、型安全性の考慮からTの消去型|T|を上限とするワイルドカード型をパラメーターとするClassが得られるという仕様になっているのだと思います。
public class FakeClassFactory<T> { T data; public FakeClassFactory(T data) { this.data = data; } @SuppressWarnings("unchecked") // really unsafe public Class<? extends T> getClassUnsafe() { // もし言語仕様がClass<? extends T>ということになっていたらこのキャストは不要になる。 return (Class<? extends T>)data.getClass(); } public static void main(String[] args) throws Exception { List<String> strList = new ArrayList<String>(); List<Integer> intList = new ArrayList<Integer>(); FakeClassFactory<List<String>> fakeStrListClassFactory = new FakeClassFactory<List<String>>(strList); Class<? extends List<String>> fakeStrListClass = fakeStrListClassFactory.getClassUnsafe(); FakeClassFactory<List<Integer>> fakeIntListClassFactory = new FakeClassFactory<List<Integer>>(intList); Class<? extends List<Integer>> fakeIntListClass = fakeIntListClassFactory.getClassUnsafe(); // 本来不正なキャストだがこの行で警告にならない。 strList = fakeStrListClass.cast(intList); strList.add("test"); int value = intList.get(0); // ClassCastException } }
Class.cast()やClass.isInstance()メソッドの存在意義について
存在意義がなかなか理解しにくいものとしてClassクラスのcast()メソッドやisInstance()メソッドがあります。普通のキャスト演算子やinstanceof演算子があるのにどうしてわざわざこのようなメソッドが存在しているのでしょう?これは以下の例を考えるとよくわかります。この例では任意の型のコレクションから、特定の型のインスタンスの要素を抜き出してコピーしようとするものです。
public static void main(String[] args) throws Exception { List<String> strArray = Arrays.asList("test1", "test2"); List<Integer> intArray = new ArrayList<Integer>(); notSafe(strArray, intArray); Integer value = intArray.get(0); // ClassCastException } public static <T> void notSafe(Collection<?> src, Collection<? super T> dest) { for (Object o : src) { if (o instanceof T) { // コンパイルエラー T t = (T) o; // このキャストは実は見せかけでこの行ではClassCastExceptionにならない。 dest.add(t); // コンパイル時に型がチェックされずに不正な型が格納されてしまう。 } } }
この例は2重の問題があります。まず、型Tに関してはinstanceof演算子の記述でコンパイルエラーとなります。instanceofは実行時の型を調べるための命令ですが、Tはこの例では見かけ上Integer型にバインドされますが、型消去により実際はObject型しか残らないため、命令が実行できないのです。仮にこのinstanceofのチェックをコメントアウトしたとしても、今度は型Tに対するキャストの行で警告が残ります。コンパイラーはこのキャストの行で本来意図している型へのキャストのコードを生成できないため、正しく型を判断することができません。結果として上記の例ではまったく別の行でClassCastExceptionが発生してしまいます。
この場合の問題は型Tに関する情報が実行時に残らないことにあります。この場合解決策としてはClass
public static <T> void safe(Collection<?> src, Collection<? super T> dest, Class<T> c) { for (Object o : src) { if (c.isInstance(o)) { T t = c.cast(o); // もしキャストが不正ならこの行で例外がでる。 dest.add(t); } } }
これだと、実行時に正しくインスタンスの型判定をしたり、キャストしたりすることができます。この場合、仮にif文をコメントアウトしても、今度はキャストの行でClassCastExceptionが発生しますので、広い意味で型安全と言えるわけです。
ただし、Classクラスのこれらのメソッドにももちろん限界があり、List