昔も書いたが、EclipseのJavaコンパイラとJDKのjavaコンパイラは別物。
昨日、原因究明に到るまで半日要したのだが、
Eclipse3.5のJavaコンパイラ(ecj)に致命的なバグがあることを発見した。
簡単に説明すると、
『非publicな親クラスから引き継いだpublicメンバーを、異なるパッケージにおいて子クラス経由でリフレクションで呼び出すとエラーになる』
というもの。まあコードを見たほうが早いので、下のコードを見るべし。
サンプルコード
NonPublicParent.java
package foo; class NonPublicParent { public void aMethod() { System.out.println("method1"); } }
PublicChild.java
package foo; public class PublicChild extends NonPublicParent { }
FooMain.java
package foo; import java.lang.reflect.Method; public class FooMain { public static void main(String[] args) throws Exception { final PublicChild pc = new PublicChild(); // -------------------------------------- // 普通に呼び出す。当然ながら正常動作する // -------------------------------------- pc.aMethod(); // -------------------------------------- // リフレクションで呼び出す。こちらも正常動作する。 // -------------------------------------- Method aMethod = PublicChild.class.getMethod("aMethod", new Class[]{}); aMethod.invoke(pc, new Object[]{}); } }
BarMain.java(パッケージが異なることに注意)
package bar; import java.lang.reflect.Method; import foo.PublicChild; public class BarMain { public static void main(String[] args) throws Exception { final PublicChild pc = new PublicChild(); // -------------------------------------- // 普通に呼び出す。当然ながら正常動作する // -------------------------------------- pc.aMethod(); // -------------------------------------- // リフレクションで呼び出す。動作しない。 // -------------------------------------- Method aMethod = PublicChild.class.getMethod("aMethod", new Class[]{}); aMethod.invoke(pc, new Object[]{}); // ←ここで例外 } }
このとき、FooMainの実行結果は以下のとおり。まあごく当たり前の結果。
method1 method1
しかし、BarMainの実行結果はこの通りだ。
method1 Exception in thread "main" java.lang.IllegalAccessException: Class bar.BarMain can not access a member of class foo.NonPublicParent with modifiers "public" at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65) at java.lang.reflect.Method.invoke(Method.java:588) at bar.BarMain.main(BarMain.java:23)
何故かIllegalAccessExceptionが発生する。
ひとつ上で、実際にコンパイル可能なコードの実行が成功している(当たり前だが)のにもかかわらず。
原因考察
Java言語仕様的には、非publicクラスから継承したpublicメンバーは、子クラスのpublicメンバーとして扱われる。
だから、
pc.method1();
は何の問題もなくコンパイルが成功する。
最初は、通常のメソッド呼び出し"pc.aMethod"()
はコンパイルもでき、もちろん正常に動作するのに、
リフレクションだと失敗するというのが信じられなかった。
自分が何か、致命的な勘違いをしているのではないか、と。
しかし試行錯誤の末、javacでコンパイルした場合は正常に動作することを発見し、それが突破口になった。
javapの比較
ecjでコンパイルしたPublicChild
Compiled from "PublicChild.java" public class foo.PublicChild extends foo.NonPublicParent{ public foo.PublicChild(); Code: 0: aload_0 1: invokespecial #8; //Method foo/NonPublicParent."<init>":()V 4: return }
javacでコンパイルしたPublicChild
(JDk1.6.0_u20)
Compiled from "PublicChild.java" public class foo.PublicChild extends foo.NonPublicParent{ public foo.PublicChild(); Code: 0: aload_0 1: invokespecial #1; //Method foo/NonPublicParent."<init>":()V 4: return public void aMethod(); Code: 0: aload_0 1: invokespecial #2; //Method foo/NonPublicParent.aMethod:()V 4: return }
この通り、違いが明らかだ。
ecjのコンパイル結果にはaMethod
が存在しない。
どうやらこの問題、
かなり昔にSunのメーリングリストでバグレポートが上がっていた模様。
例えばコレとか。
SunJDKでは当然ながら修正済みなんだが、ecjでは発生しているということは
もしかしてecjとjavacはコードベースが共通なのか?
歴史的にはそんなことはないハズなんだが。。。