bitter_foxさんが、Java Puzzlers Advent Calendar 2016の14日目としてnesting printhingという記事を書いています。
Javaのバージョンによってコンパイルできるかどうかが異なる(JDK1.7だけバグっていてコンパイルできる)という意表を突いた問題で、このバグがどういうときに起こるかというのもなかなか理解が大変でした。
が、それよりも気になったのが、コンパイルできる状態にした上で、なぜそのような挙動をするのかというところです。
簡単に書くと、以下のようなソースですね。
class Main { public void print() { System.out.println("main"); } public class Inner { } public void run() { new Main() { @Override public void print() { System.out.println("overrideMain"); } }.new Inner() { public void test() { print(); // Main.this.print();とも書ける } }.test(); } }
これをnew Main().run();として実行すると「main」が表示されます!
new Innerのレシーバーとして、new Mainで無名内部クラス(匿名クラス)を作ってprintメソッドをオーバーライドしているので、そちらが呼ばれそうな気がするのですが、そうはなりません。
元のbitter_foxさんの記事によれば、testメソッドの中は「Main main = Main.this; main.print();」と書けるからMainが呼ばれるんだよ…という感じなんですが、それでも納得いきません。
普通であれば、変数に宣言されたクラスがどうであれ、実際のインスタンスのクラスでオーバーライドされているメソッドが呼ばれるわけですから。
結局のところ、ポイントは、new Inner(無名内部クラス(匿名クラス))から見える範囲(スコープ)はどこか、という話のようです。
単なる内部クラス(Inner)であれば、外側のクラスであるMainのメソッドを呼び出すことが出来ます。
しかし無名内部クラス(new Inner)の場合、見える範囲は、その無名内部クラスが属しているメソッド(上の例ではrunメソッド)の範囲になるようです。
で、runメソッドが属しているのはMainクラスなので、runから見えるprintというのは、Mainクラスのprintメソッドを指すことになります。(new Mainに何が書かれていても、new Innerの中からは見えない)
なので、以下のように「new Innerが入っているメソッド」をMain以外のクラスに置くと、printメソッドは呼び出せなくなります。(コンパイルエラーになる)
class Another { public void run() { new Main().new Inner() { public void test() { print(); // コンパイルエラー Main.this.print(); // コンパイルエラー } }.test(); } }
なお、以下のようにInnerの中でMainのprintメソッドを呼んでいれば、Mainを継承した無名内部クラスを作った場合にはそのメソッドが素直に呼ばれます。
class Main { public void print() { System.out.println("main"); } public class Inner { public void test() { print(); } } public void run() { new Main() { @Override public void print() { System.out.println("overrideMain"); } }.new Inner().test(); } }
いやあ、いろいろ知らないことがあって、秀逸な問題でした。