今回のネタはJavaのクラスファイルの実装について重箱の隅をつついたものです.
Javaが嫌いな方は読んだつもりになってブラウザバック!
私はJavaが好きだ.
ある日の昼下がり,私はコーヒーを片手にJavaのクラスファイルを眺めていた.
後輩の研究でJava実行ログの解析中に,想定外のメソッドが呼び出された形跡があるという報告を受けて,その原因調査をしているのだ.
ソースコードを見ても特段異常を感じなかったため,クラスファイルを直接読むこととした.
その研究では実行ログを生成するために,バイトコードを加工しているのだ.加工によって何か不具合が起きた可能性もある.
クラス中に気になるメソッドを見つけた私はコーヒーを置いた
「このメソッド,ソースコードには記述が無いな」
今日の話はこの謎のメソッドについて
Javaにはクラスやそのメンバへのアクセスを制限する修飾子があります.
キーワード | アクセス可能な範囲 |
---|---|
public | どこからでも |
protected | 同一パッケージ内及び子クラス |
(指定なし) | 同一パッケージ内 |
private | 自クラス内 |
参考: jls-8.4.3, jls-6.6.1, jls-6.6.2
これらの可視性はコンパイラによってクラスファイル中で対応するフラグに変換され,JVMはこのフラグを頼りにアクセスを管理しています.
キーワード | 対応するフラグ |
---|---|
public | ACC_PUBLIC |
protected | ACC_PROTECTED |
(指定なし) | なし |
private | ACC_PRIVATE |
参考:Table 4.6-A
Javaには先ほどのフラグでは直接実現できないようなアクセス権の範囲もあります.
そう,ネストクラスとエンクロージングクラス間のアクセスです.
public class Enclosing {
static void enclosingMethod() {
new Nested().nestedMethod();
}
static class Nested {
private void nestedMethod() {
System.out.println("Nested!");
}
}
}
皆さんの多くが険しい表情をしていると思います.
†Java完全にマスター†した各位でもネストしたクラスの挙動には自信がない人は多いのではないでしょうか.
特にstatic修飾子に関する挙動は非常に直感に反するもので,staticの付かない匿名クラス以外のインナークラスなどは,開発者のミスを誘発する以外に一体どのような用途を想定してデザイン(以下私の個人的な文句数行を省略)
話を戻します.
ネストしたクラス間では可視性を表す修飾子に関係なく互いのメンバにアクセスすることができます.
ネストしたクラスには実はいくつか分類がありますが,
- インナークラス
- staticでないメンバクラス
- ローカルクラス
- 匿名クラス
- staticなメンバクラス
ここでは(私が最も癖が少ないと感じている)staticなメンバクラスを例に挙げました.
この例ではNested#nestedMethod()
メソッドはprivateで宣言されていますが,
enclosingMethod()
メソッド中から呼び出すことができます.
ネストしたクラスはソースコード上では1つのファイルとして記述される一方,クラスファイルはネストクラスとエンクロージングクラスでそれぞれ別のファイルとなります.
では,互いのクラスからはアクセスできるが,それ以外のクラスからはアクセスできないメンバは,クラスファイルではどのようにして実現されているのでしょうか.
これが今回のネタです.
もうお気づきと思いますが,この複雑なアクセス範囲を実現するトリックが冒頭の謎のメソッドです.
上のソースコードをコンパイルするとEnclosing.class
,Enclosing$Nested.class
という2つのクラスファイルが生成されます.
それぞれのクラスファイルの内容をjavap
で確認し,メソッド部分を以下に抜粋すると以下の様になります.
public class Enclosing
{
public Enclosing();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static void enclosingMethod();
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #2 // class Enclosing$Nested
3: dup
4: invokespecial #3 // Method Enclosing$Nested."<init>":()V
7: invokestatic #4 // Method Enclosing$Nested.access$000:(LEnclosing$Nested;)V
10: return
}
class Enclosing$Nested
{
Enclosing$Nested();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return
private void nestedMethod();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Nested!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
static void access$000(Enclosing$Nested);
descriptor: (LEnclosing$Nested;)V
flags: ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method nestedMethod:()V
4: return
}
さて,問題のメソッドは...
なに?読めない?はい.Javaっぽい擬似コードにします.
public class Enclosing
{
public Enclosing() {
super();
}
static void enclosingMethod() {
Nested nested = new Nested();
Nested.access$000(nested);
}
}
class Nested
{
Nested() {
super();
}
private void nestedMethod() {
System.out.println("Nested!");
}
static void access$000(Nested nested) {
nested.nestedMethod();
}
}
こうなってしまえば後は簡単,仕組みをあえて解説する必要もないほどですね.
さて,気を取り直して.ソースコードに記述のないメソッドが3つほどありますね.
Enclosing()
,Nested()
はコンストラクタを明示的に定義しないときに追加される暗黙のコンストラクタというやつです.
残りの1つ,access$000
などという物々しい名前のが問題のメソッドです.
実は$
記号は普通にメソッド名として使えるので(使わないほうが良いが)ただのstaticメソッドです.
内容もnestedMethod()
をただ呼び出しているだけ.Nested
クラス内なのでprivateでも問題なく呼び出せます.
普通のメソッドと異なる点はACC_SYNTHETICというフラグが付いていることです(擬似コードには無いのでjavapの結果のほうを参照).
このフラグはソースコードに存在しない,コンパイラによって自動でつくられたメソッドにつくものです.
参考:Table 4.6-A
コンパイラによって合成されるということは,Enclosing.java
をコンパイルするまでこのアクセス用のメソッドの存在は他のクラスにはわかりません.
従って,access$000
はEnclosing
クラスからしか呼び出せないため,目標のアクセス範囲が実現しています.
ネストクラスからエンクロージングへのアクセスやフィールドへのアクセスも同様の方法で実現できます.
無駄な知識が付きましたね!
さらに無駄な知識をあなたに.
アクセスを制限するこの仕組みの詳細な実装は,実は仕様で規定されていません.
今回はstaticなメソッドが合成されましたが,まわりのコードによっては,staticが付いていなかったり,エンクロージングクラスの別の箇所で使われる匿名クラスを引数にとるメソッドとすることで他クラスからのアクセスを防いだり,さまざまなバリエーションがあります.
今回はJDK 1.8.0_121
によってコンパイルされたクラスファイルを載せましたが,Eclipseのコンパイラはまた趣向の違うクラスファイルを作ります.
是非皆さんも,コーヒー片手にクラスファイルを読んで,Javaへの愛を深めましょう.