nesting printhing
このエントリは Java Puzzlers Advent Calendar 2016 の 14 日目です。
class Main { abstract class Person { protected String name; public Person(String name) { this.name = name; } public abstract void print(); } protected void print(String s) { System.out.println("[" + s + "]"); } public void run() { Person p = new Main() { protected void print(String s) { System.out.println("\"" + s + "\""); } }.new Person("foobar") { public void print() { System.out.println("This is Person class: "); print(name); } }; p.print(); } public static void main(String[] args) { new Main().run(); } }
1. This is Person class: [foobar] 2. This is Person class: "foobar" 3. コンパイル時エラー 4. 実行時例外
解説は後ほど
Twitterでアンケートを取ると,以下のような結果になりました.
結果を見ると2. "foobar"が48%と一番多く,僅差で3. コンパイル時エラーが35%と多いですね.
1. [foobar]は15%と少なく,4. 実行時例外はごくわずかでした.
それでは答えです.
javacでコンパイルして,javaで動かすと,下の画像のようになります.
なんと,予想の少なかった1. [foobar]が正解ですね.
.
.
.
そろそろ「ちょっと待った!手元で動かしたら,コンパイル時エラーになるんだけど!!!」という声が聞こえてきそうです.
おや,おかしいですね.
先ほど動かしたJavaのバージョンを確認してみましょう.
JDK7での結果は1. [foobar]のようでした.
それではJDK8ではどのような結果になるのでしょうか.
実行結果がこちらです.
仰るとおり,3. コンパイル時エラーになりました.
どうやらJDKのバージョンによって挙動が違うようです.
JDKのバージョンで挙動が違うこのコードですが,Java言語の仕様としてはどちらが正しいのでしょうか(言語仕様が変わったという可能性もありますが,今回のコードについては違います).
JDK8では適切なメソッドが見つからなかったというエラーなので,メソッドの選択についての言語仕様を見てみましょう.
15.12. Method Invocation Expressionsの15.12.1. Compile-Time Step 1: Determine Class or Interface to Searchです.
If the form is MethodName, that is, just an Identifier, then: If the Identifier appears in the scope of a visible method declaration with that name (§6.3, §6.4.1), then: If there is an enclosing type declaration of which that method is a member, let T be the innermost such type declaration. The class or interface to search is T. This search policy is called the "comb rule". It effectively looks for methods in a nested class's superclass hierarchy before looking for methods in an enclosing class and its superclass hierarchy. See §6.5.7.1 for an example.
賢くなったGoogle翻訳で翻訳すると
フォームがMethodNameの場合、つまり識別子だけの場合は、次のようになります。 識別子が、その名前(§6.3、§6.4.1)を持つ可視メソッド宣言のスコープに現れた場合は、次のようになります。 そのメソッドがメンバーである囲み型宣言がある場合、Tをそのような型宣言の最も内側の宣言とします。 検索するクラスまたはインタフェースはTです。 この検索ポリシーは「くしルール」と呼ばれます。 これは、囲みクラスとそのスーパークラス階層のメソッドを探す前に、ネストされたクラスのスーパークラス階層のメソッドを効果的に探します。 例については、§6.5.7.1を参照してください。
つまり,「クラスがネストされていた場合,そのクラスか親クラスからメソッド名で検索するよ.見つかったら,外の環境にあるメソッドは検索しないよ」という意味です.
今回のコードの場合,printという名前のメソッドがすでにPersonにあるので,Mainのprintを見ないというのが正しい挙動のようです.
ですので,JDK7の挙動はバグであり,JDK8で修正された結果,挙動が変わっています.
バグについては,こちらの記事をご参照ください.
ということで,3. コンパイル時エラーが正解でした.
ここから先は余談です.
Mainのprintを呼び出せるようにするには,Main.printかPerson.printの名前を変更すればよいです(Main.this.printとして呼び出しても良い).
Main.printの名前を変えて動かしてみましょう.もちろんJDK8で動かします.
ちゃんと動くように修正したため,挙動は1. [foobar]か2. "foobar"のいずれかです(例外は起きそうにないですね).
動かしてみると,投票結果では一番多かった2. "foobar"を差し置いて,1. [foobar]が出力されました.
printNameメソッドは,直前のnew Main() {...}ではなく,runを実行しているMainクラスをレシーバとして実行するため,このような挙動になりました.
以下のようにMain.thisを補うと,どれを参照しているのかが分かりやすくなると思います.
new Person("foobar") { @Override public void print() { System.out.println("This is Person class: "); Main main = Main.this; main.printName(name); } };
以下のように,runメソッドを実行するMainクラスのprintNameの実装を変えると,2. "foobar"が出力されます.
class Main { abstract class Person { protected String name; public Person(String name) { this.name = name; } public abstract void print(); } protected void printName(String s) { System.out.println("[" + s + "]"); } public void run() { Person p = new Person("foobar") { @Override public void print() { System.out.println("This is Person class: "); printName(name); } }; p.print(); } public static void main(String[] args) { new Main() { @Override protected void printName(String s) { System.out.println("\"" + s + "\""); } }.run(); } }
この問題をまとめると,「こんな分かりにくいコードを書くな」に尽きるでしょうか.