1. 為什么要使用內部類
內部類就是定義在一個類內部的類,那么為什么要使用內部類呢?主要原因有以下幾點:第一,內部類中定義的方法能訪問到它所在外部類的私有屬性及方法;第二,外部類無法實現對同一包中的其他類隱藏,而內部類可以做到這一點;第三,匿名內部類在我們只需使用該類的實例依次時可以有效減少我們的代碼量。關於以上三點,我們在下文中會舉出具體例子進行進一步的說明。
2. 如何使用內部類
(1)使用內部類訪問外圍類私有變量
在內部類中,我們能夠訪問到它所在外部類中的私有實例變量及方法,請看以下代碼:
1 public class Outer { 2 private int own = 1; 3 public void outerMethod() { 4 System.out.println("In Outer class"); 5 Inner inner = new Inner(); 6 inner.innerMethod(); 7 } 8 public static void main(String[] args) { 9 Outer outer = new Outer(); 10 outer.outerMethod(); 11 } 12 13 private class Inner { 14 public void innerMethod() { 15 System.out.println("The var own in Outer is " + own); 16 } 17 } 18 }
這段代碼的輸出如下所示:
我們可以看到,在內部類中確實訪問到了外部類Outer的private變量own。那么,這是如何做到的呢?實際上,內部類對象隱式地持有一個外部類對象的引用,我們假設這個引用名為outer,那么實際上內部類的innerMethod方法是這樣子的:
1 public void innerMethod() { 2 System.out.println("The var own in Outer is " + <strong>outer</strong>.own); 3 }
編譯器會修改Inner類的構造器,添加一個外部類Outer的引用作為參數,大概是這個樣子:
1 public Inner(Outer outer) { 2 this.outer = outer; 3 }
所以我們在Outer類的outerMethod方法中調用Inner構造器那條語句實際上會被編譯器“改成“這個樣子:
1 Inner inner = new Inner(this);
我們來通過javap看下生成的字節碼,來直觀地感受下:
我們重點看一下這一行:
我們可以看到,調用Inner類的構造方法時,確實傳入了類型為Outer的參數(即外圍類的引用)。
我們還可以看到,編譯器為這個類生成了一個名為access$100的靜態方法,在這個方法中,加載並獲取了own變量。實際上,內部類就會調用這個方法來獲取外部類的私有實例變量own。
我們再來看下編譯器為內部類生成的字節碼:
我們來看一下標號16和19的行,確實是現獲取外圍類引用,然后調用了access$100方法,並傳入了外圍類引用作為參數,從而在內部類中能夠訪問外圍類中的private變量。
(2)內部類的特殊語法規則
實際上,使用外圍類引用的正規語法規則如下所示:
1 OuterClass.this
例如,以上Inner類的innerMethod方法我們使用正規語法應該這么寫:
public void innerMethod() { System.out.println("The var own in Outer is " + Outer.this.own); }
另一方面,我么也可以采用以下語法更加明確地初始化內部類:
Inner inner = this.new Inner();
我們還可以顯示的將內部類持有的外圍類引用指向其它的外圍類對象,假設outerObject是一個Outer類實例,我們可以這樣寫:
Outer.Inner inner = outerObject.new Inner();
這樣一來,inner所持有的外圍類對象引用即為outerObject。
在外圍類的作用域之外,我們還可以像下面這樣引用它的內部類:
OuterClass.InnerClass
(3)局部內部類
具備內部類即定義在一個方法內部的類,如以下代碼所示:
1 public class Outer { 2 private int own = 1; 3 public void outerMethod() { 4 class Inner { 5 public void innerMethod() { 6 System.out.println("The var own in Outer is " + own); 7 } 8 } 9 System.out.println("In Outer class"); 10 Inner inner = new Inner(); 11 inner.innerMethod(); 12 } 13 public static void main(String[] args) { 14 Outer outer = new Outer(); 15 outer.outerMethod(); 16 } 17 }
局部類的作用域就被限制在定義它的方法的方法體中,因此它不能用public或private訪問修飾符來修飾。
與常規內部類比較,局部類具有一個優勢:可以訪問局部變量。但是這有一個限制,就是它訪問的局部變量必須被聲明為final。簡單地說,這是出於一致性的考慮。因為局部變量的生命周期隨着方法的運行結束也隨之結束了,而局部類的生命周期卻不會隨着方法的結束而結束。在方法運行完后,局部類為了能夠繼續訪問局部變量,需要對局部變量進行備份。
實際上,在創建局部類的對象時,編譯器會隱式修改具備類的構造器,並將局部類要訪問的“外部變量”作為參數傳遞給它,這樣具備類可以在其內部創建一個拷貝並存儲在自己的實例域中。設想若這個變量不是final的,即我們可以在具備類對它進行修改,這無疑會破壞數據的一致性(局部變量與其在局部類內部的拷貝版本不一樣)。所以想讓局部類訪問的變量必須加上final修飾符。
(4)匿名內部類
對於只需要實例化一次的類,我們可以不給它命名,而是通過匿名內部類的形式來使用。匿名內部類的語法形式如下:
new SuperType(construction parameters) { inner class methods and data }
匿名類不能有構造器,因此將構造器參數傳給超類的構造器(SuperType)。匿名類內部可以定義一些方法與屬性。
還有一種形式的匿名內部類是實現了某種接口,它的語法格式如下:
new Interface() { methods and data }
注意,以上代碼的含義並不是實例化一個接口,而是實例化實現了某種接口的匿名內部類。
我們上面提到的傳遞給Time的構造器的參數之一是一個實現了ActionListener接口的類對象,顯然那個類只需要實例化一次,因此我們可以用匿名內部類來實例化:
... ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) { ... } };
(5)靜態內部類
有時候,我們不想讓一個內部類持有外圍類對象的引用,這是我們可以選擇使用靜態內部類。靜態內部類不會持有外圍類的引用,而非靜態的內部類都會持有外圍類對象的引用(隱式持有),而這也是導致內存泄露(Memory Leak)的一個常見原因之一。
請看以下代碼:
1 public class Outer { 2 private int own = 1; 3 public void outerMethod() { } 4 public static void main(String[] args) { } 5 6 private class Inner { 7 public void innerMethod() { } 8 } 9 }
可以看到,Inner類內部持有一個Outer類的引用。
現在我們給Inner類加上static修飾符,讓它變為一個靜態內部類,再來看一下:
可以看到,現在內部類不再持有外圍類Outer的引用了。
