Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

インナークラスからアウタークラスのprivateメソッドへのアクセスでの奇妙なバイトコード

JVM Language Summit 2017のセッション動画を見ていると、おもしろいコードがありました。

youtu.be

JEP 181: Introduce "nests" as an explicit access control contextに関するセッションです。

こんな感じのコードです。mainメソッドは僕が追加しています。

public class Outer {

    private void m_outerpriv() {
        System.out.println("called m_outerpriv");
    }

    class Inner {
        public void test() {
            new Outer().m_outerpriv();
        }
    }

    public static void main(String[] args) {
        new Outer().new Inner().test();
    }
}

当然実行できます。

$ javac Outer.java

$ java Outer
called m_outerpriv

さて、どんなバイトコードになっていると思いますか?Inner#test()からOuterのメソッドm_outerpriv()への呼び出しはprivateメソッドですしinvokespecialでしょうか?

現時点での仕様では違います。invokestaticになります。

$ javap -p -c Outer\$Inner
Compiled from "Outer.java"
class Outer$Inner {
  final Outer this$0;

  Outer$Inner(Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:LOuter;
       5: aload_0
       6: invokespecial #2                  // Method java/lang/Object."<init>":()V
       9: return

  public void test();
    Code:
       0: new           #3                  // class Outer
       3: dup
       4: invokespecial #4                  // Method Outer."<init>":()V
       7: invokestatic  #5                  // Method Outer.access$000:(LOuter;)V
      10: return
}

access$000というstaticメソッドを呼び出しています。これは何でしょうか。

$ javap -p -c Outer
Compiled from "Outer.java"
public class Outer {
  public Outer();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method java/lang/Object."<init>":()V
       4: return

  private void m_outerpriv();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String called m_outerpriv
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
...
  static void access$000(Outer);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method m_outerpriv:()V
       4: return
}

Inner#test()メソッドではOuterのstaticメソッドaccess$000を呼び出します。引数にOuterインスタンスを渡します。access$000ではそのインスタンスを通じてprivateメソッドをinvokespecialで呼び出します。コンパイラが生成しているのでaccess$000メソッドはflags: ACC_STATIC, ACC_SYNTHETICです。

どうしてこうなるのでしょうか?Java Language Specification(JLS)の6.6.1です。

Otherwise, the member or constructor is declared private, and access is permitted if and only if it occurs within the body of the top level type (§7.6) that encloses the declaration of the member or constructor.

https://docs.oracle.com/javase/specs/jls/se9/html/jls-6.html#jls-6.6.1

privateで宣言しているメンバやコンストラクタは、メンバやコンストラクタの宣言を含んでいるトップレベルの型の内部からのアクセスの場合に限って許可される。

つまり、インナークラスはここでのトップレベルの型ではないため、Outerのprivateメソッドに直接呼び出せません。なのでコンパイラが合成メソッドを生成して、Outerのstaticメソッドから呼び出すという回りくどいことをしています。staticメソッドはOuterのメンバであるため、privateメソッドが呼び出せます。

同様に、Java Virtual Machine Specificationにも記述があります。5.4.4です。

A field or method R is accessible to a class or interface D if and only if any of the following is true: ... R is private and is declared in D.

https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.4.4

フィールドまたはメソッドRにアクセス可能なクラスまたはインタフェースDとは、以下のことが真となる場合のみに限る。

  • Rがprivateでその宣言がDにある。

上述のJEP 181では、nestしているものからでもprivateなものにアクセスできるようにする、という提案です。2013年には提案が作成されているのですが!まだドラフトで仕様となるにはまだまだかかりそうです(プライオリティもそれほど高くないため)。提案ではstaticメソッドを合成せず、インナークラスのメソッドでは単にアウタークラスのインスタンスを生成してinvokespecialでprivateメソッドをで呼び出すことができるようになります。