1. 程式人生 > >Java核心技術點之內部類

Java核心技術點之內部類

為什麼要使用內部類

內部類就是定義在一個類內部的類,那麼為什麼要使用內部類呢?主要原因有以下幾點:

  • 第一,內部類中定義的方法能訪問到外圍類的私有屬性及方法;
  • 第二,外圍類無法實現對同一包中的其他類隱藏,而內部類可以做到這一點;
  • 第三,匿名內部類在我們只需使用該類的例項一次時可以有效減少我們的程式碼量。

關於以上三點,我們在下文中會舉出具體例子進行進一步的說明。

如何使用內部類

使用內部類訪問外圍類私有變數

在內部類中,我們能夠訪問到它的外圍類中的私有例項變數及方法,請看以下程式碼:

 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 " + outer.own);
3 }

編譯器會修改Inner類的構造器,新增一個外部類Outer的引用作為引數,大概是這個樣子:

1 public Inner(Outer outer) {
2     this.outer = outer;
3 }

所以我們在Outer類的outerMethod方法中呼叫Inner構造器那條語句實際上會被編譯器“改成“這個樣子:

Inner inner = new Inner(this);

我們來通過javap看下生成的位元組碼,來直觀地感受下:



我們重點看一下這一行:


我們可以看到,呼叫Inner類的構造方法時,確實傳入了型別為Outer的引數(即外圍類的引用)。

我們還可以看到,編譯器為這個類生成了一個名為access$100的靜態方法,在這個方法中,載入並獲取了own變數。實際上,內部類就會呼叫這個方法來獲取外部類的私有例項變數own。
我們再來看下編譯器為內部類生成的位元組碼:


我們來看一下標號16和19的行,確實是先獲取外圍類引用,然後呼叫了access$100方法,並傳入了外圍類引用作為引數,從而在內部類中能夠訪問外圍類中的private變數。

內部類的特殊語法規則

實際上,使用外圍類引用的正規語法規則如下所示:

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

區域性內部類

區域性內部類即定義在一個方法內部的類,如以下程式碼所示:

 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修飾符。

匿名內部類

對於只需要例項化一次的類,我們可以不給它命名,而是通過匿名內部類的形式來使用。匿名內部類的語法形式如下:

new SuperType(construction parameters) {
    inner class methods and data}

匿名類不能有構造器,因此將構造器引數傳給超類的構造器(SuperType)。匿名類內部可以定義一些方法與屬性。
還有一種形式的匿名內部類是實現了某種介面,它的語法格式如下:

new Interface() {
    methods and data
}

注意,以上程式碼的含義並不是例項化一個介面,而是例項化實現了某種介面的匿名內部類。

靜態內部類

有時候,我們不想讓一個內部類持有外圍類物件的引用,這是我們可以選擇使用靜態內部類。靜態內部類不會持有外圍類的引用,而非靜態的內部類都會持有外圍類物件的引用(隱式持有),而這也是導致記憶體洩露(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是非靜態的,我們用javap檢視下編譯器生成的相應class檔案:


可以看到,Inner類內部持有一個Outer類的引用。

現在我們給Inner類加上static修飾符,讓它變為一個靜態內部類,再來看一下:


可以看到,現在內部類不再持有外圍類Outer的引用了。