JVM Language Summit 2017のセッション動画を見ていると、おもしろいコードがありました。
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メソッドをで呼び出すことができるようになります。