SunのコンパイラとEclipse JDTで動きが違う

HudsonがEclipseコンパイルできなかったという話題が1か月前くらいにあって、その調査をしているときにjavacとJDTで微妙に動作が違う点を見つけました。

class Hoge<T> {
    void h() {
        class Foo { T t; }
        Foo o = new Foo();
        boolean b = (o instanceof Foo); // JDTでエラー
        T t = o.t; // javacでエラー
    }
}

これはどうもローカルクラスの扱いがjavacとJDTで違うようです。

JDT

まず、(o instanceof Foo)でエラーになるJDTを見てみると、次のようなエラーメッセージが出ていました。

Cannot perform instanceof check against parameterized type Foo. Use instead its raw form Foo since generic type information will be erased at runtime

メッセージを参考にして考えてみると、次のあたりが関係しそうです。

The type of a RelationalExpression operand of the instanceof operator must be a reference type or the null type; otherwise, a compile-time error occurs. The ReferenceType mentioned after the instanceof operator must denote a reference type; otherwise, a compile-time error occurs. It is a compile-time error if the ReferenceType mentioned after the instanceof operator does not denote a reifiable type (§4.7).

15.20.2 Type Comparison Operator instanceof

「instanceofの右側に来る型はreifiable typeでないとコンパイルエラー」とあります。
で、reifiable typeとは何かというと、ざっくりいってしまえば「クラスファイルを作っても情報が落ちない型」のことです。

Because some type information is erased during compilation, not all types are available at run time. Types that are completely available at run time are known as reifiable types. A type is reifiable if and only if one of the following holds:

  • It refers to a non-generic type declaration.
  • It is a parameterized type in which all type arguments are unbounded wildcards (§4.5.1).
  • It is a raw type (§4.8).
  • It is a primitive type (§4.2).
  • It is an array type (§10.1) whose component type is reifiable.
4.7 Reifiable Types

class Fooの宣言をよく見てみると、

class Hoge<T> {
    void h() {
        class Foo {}
    }
}

といった具合にHoge< T >というgeneric type declarationのコンテキストを引きずっています。JDTはこのFooをgeneric type declarationとみなして、Reifiable Typeでないためにinstanceofでエラーが発生していました。

ちなみに、(o instanceof Hoge.Foo)と書ければこの問題は回避できますが、ローカルクラスはそんなことできないのでもうどうしようもありません。

javac

javacのほうも見てみると、違うところでエラーになります。

src\Hoge.java:6: 互換性のない型
検出値  : java.lang.Object
期待値  : T
        T t = o.t; // javacでエラー
               ^
エラー 1 個

Foo#tはT型のはずなのに、上記のように怒られてしまいます。逆に、(o instanceof Foo)ではエラーになりません。

o.tがjava.lang.Object型と判定されているところを見ると、FooはどうもRaw Typeとみなされているようです。つまり、Hoge< T >のコンテキストを引き継いでいない模様。

To facilitate interfacing with non-generic legacy code, it is also possible to use as a type the erasure (§4.6) of a parameterized type (§4.5). Such a type is called a raw type.

More precisely, a raw type is define to be either:

  • The name of a generic type declaration used without any accompanying actual type parameters.
  • Any non-static type member of a raw type R that is not inherited from a superclass or superinterface of R.
4.8 Raw Types

Tのerasureはjava.lang.Objectなので、FooがRaw Typeとみなされたと考えるのが順当かと思います。

まとめ

どちらもヒープ汚染は発生しえないし、そもそも私はローカルクラスを使ったことがないのでどうでもよい問題なのですが、どちらが正しい動作なのか言語仕様書では判断できませんでした。
ただ、javacのほうはFooの中でTという型を利用できるのに、外からは全く使えないのでかなり意味が不明です。JDTのほうもinstanceofを封じられるので、Java5以降でローカルクラスは使わないほうがよいかもしれません。

正しい解釈をご存知の方がいらっしゃればコメントくださいませ。