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 Expressions15.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();
    }
}

この問題をまとめると,「こんな分かりにくいコードを書くな」に尽きるでしょうか.