Java 內部類實現原理簡單分析
轉載:原文地址http://www.fzhen.info/?p=300
本文重點不在與內部類的語法及使用,而是試圖解釋一些背後的原理。
內部類簡介
Java支援在類內部定義類,即為內部類。
普通內部類
把類的定義放在類的內部,例如:
程式碼清單1:
public class Outer{
private int outField=10;
class Inner{
void innerMethod(){
int i = outField;
}
}
}
注意到 Inner 類可以訪問 Outer 類的變數,即使是私有變數。那麼,顯然該內部類是與例項聯絡的,因為它可以訪問例項的變數。對外層例項的顯式引用為OuterClass.this。而要去建立某個內部類物件,則必須在new表示式中提供對其外部類物件的引用,使用.new語法,如程式碼清單2所示:
public class Outer{
private int outField=10;
int foo = 10;
class Inner{
int foo = 0;
void innerMethod(){
System.out.println(foo);//print 0
System.out.println(Outer.this.foo); //print 10
foo = Outer.this.foo;
System.out.println(foo);//print 0
}
}
public static void main(String[] args){
Outer oc = new Outer();
Outer.Inner ic = oc.new Inner();
ic.innerMethod();
}
}
巢狀類
如果不需要內部類物件與外圍類之間有聯絡,那麼可以將內部類宣告為static。
巢狀類與普通類基本沒有區別,除了可以訪問其外圍類的私有成員而且外圍類相當於提供了一個名字空間。
而相對與普通內部類,普通內部類的不能有static的欄位和方法,也不能包含巢狀類(但可以有static final的欄位)。
內部實現
位元組碼檔案
每個內部類被編譯成單獨的位元組碼檔案,例如編譯程式碼清單1的檔案,會生成兩個.class檔案:
$ javac Outer.java
$ ls *.class
Outer.class Outer$Inner.class
內部類的命名為Outer$Inner,對與匿名內部類,會給內部類形成一個編號,生成類似Outer$1.class這樣的檔案。
Java 8的lambda表示式雖然與內部類淵源極深,但並不會生成單獨的位元組碼檔案。
內部類如何持有外部類例項的引用
使用javap命令反編譯內部類檔案,
$ javap -v Outer\$Inner.class
選擇一部分輸出如下:
Constant pool:
#1 = Fieldref #4.#16 // Outer$Inner.this$0:LOuter;
...其他常量
{
final Outer this$0;
descriptor: LOuter;
flags: ACC_FINAL, ACC_SYNTHETIC
Outer$Inner(Outer);
descriptor: (LOuter;)V
flags:
Code:
stack=2, locals=2, args_size=2
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
......
}
可以看到內部類有一個名字為this$0的欄位,型別為Outer。 再看內部類的建構函式。我們並沒有定義建構函式,但編譯器自動生成了一個建構函式,而且帶有一個Outer型別的引數。下面逐條看生成的位元組碼指令:
aload_0: 將區域性變量表的第一個變數載入到運算元棧,對於例項方法,該變數是this指標。
aload_1: 將區域性變量表第二個變數載入到運算元棧,此處該變數是建構函式的引數,即外圍例項引用。
putfield #1: 使用運算元棧頂變數給成員變數賦值。#1是常量池序號,通過查詢常量池可知就是編譯器自動加上的this$0欄位。所以該指令的作用就是把建構函式傳進來的Outer例項付給了隱藏的this$0變數。從而內部類獲取了外部類的引用。
下面幾句呼叫父類的建構函式並返回。
因為內部類必須持有外圍類物件的引用,所以繼承內部類時也必須傳遞外圍類物件。Thinking in Java的一個例子:
class WithInner{
class Inner {}
}
public class InheritInner extends WithInner.Inner {
InheritInner(WithInner wi){
wi.super();
}
public static void main(String[] args){
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
內部類只能訪問final或相當於final的區域性變數
public class Outer2{
void foo(){
int i = 100;
class LocalInner{
void bar(){
int d = i;
// i++;
}
}
// i++;
}
}
上面程式碼在函式foo()中定義了一個內部類,在內部類訪問函式區域性變數i。兩處註釋掉的i++
都會產生編譯錯誤:從內部類引用的本地變數必須是最終變數或實際上的最終變數。
這與java對閉包的實現有關。編譯器為內部類中用到的所有區域性變數在內部類中都生成一個對應的欄位。當建立內部類的例項時,區域性變數的值會通過內部類的建構函式拷貝到內部類的對應欄位中。內部類對外圍區域性變數的訪問實際轉化成了對內部類欄位的訪問。
因為值已經被拷貝到了內部類的欄位中,那麼在外圍函式中對該變數的修改會使整個程式的行為變得有點怪異–看起來你可能使用了過期的資料。因此,不能在外圍函式中修改變數。對應的,內部類中也不允許修改該變數(實際上,自動生成的欄位被宣告為final),否則,外圍函式好像在處理過期資料。
從上面程式碼反編譯的結果可以清楚看到這一點,見輸出中的註釋。
javap -v Outer2\$1LocalInner.class
...
class Outer2$1LocalInner
...
Constant pool:
...
{
final int val$i; //生成的欄位,對應區域性變數i
descriptor: I
flags: ACC_FINAL, ACC_SYNTHETIC
final Outer2 this$0;
descriptor: LOuter2;
flags: ACC_FINAL, ACC_SYNTHETIC
Outer2$1LocalInner();
descriptor: (LOuter2;I)V //建構函式除了Outer2引數,增加了一個int引數
flags:
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LOuter2;
5: aload_0
6: iload_2
7: putfield #2 // Field val$i:I 把int引數賦值給生成的欄位
10: aload_0
11: invokespecial #3 // Method java/lang/Object."<init>":()V
14: return
LineNumberTable:
line 4: 0
Signature: #15 // ()V
void bar();
descriptor: ()V
flags:
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: getfield #2 // Field val$i:I
4: istore_1 // int d = i 對i的訪問時機訪問的是內部類的欄位val$i
5: return
LineNumberTable:
line 6: 0
line 8: 5
}
$ javap -v Outer2.class
...
public class Outer2
...
Constant pool:
...
{
void foo();
descriptor: ()V
flags:
Code:
stack=4, locals=2, args_size=1
0: bipush 100
2: istore_1
3: new #2 // class Outer2$1LocalInner
6: dup
7: aload_0 //this指標入棧
8: iload_1 //區域性變數i入棧
9: invokespecial #3 // Method Outer2$1LocalInner."<init>":(LOuter2;I)V
// 呼叫內部類的建構函式,i作為其中一個引數
12: invokevirtual #4 // Method Outer2$1LocalInner.bar:()V
15: return
LineNumberTable:
line 3: 0
line 11: 3
line 12: 15
}
SourceFile: "Outer2.java"
InnerClasses:
#7= #2; //LocalInner=class Outer2$1LocalInner