《 Thinking in Java 》第十章 內部類
可以將一個類的定義放在另一個類的定義內部,這就是內部類
- 內部類與組合是完全不同的概念。
- 內部類看起來像是一種程式碼隱藏機制,但是它還了解外圍類,並能與之通訊;
- 更優雅!!!
建立內部類
很簡單——把類的定義置於外圍類的裡面:
public class A {
class InnerA {
}
public InnerA getInnerA() {
return new InnerA();
}
}
與使用普通類的方法,沒什麼不同,很典型的一個情況如上,外部類有一個方法,返回一個指向內部類的引用。 注意:如果想從外部類的非靜態方法之外的任意位置建立某個內部類的物件,那麼必須想這樣來具體的指明這個物件的型別:
//OuterClassName.InnerClassName
public static void main(String[] args) {
A.InnerA a = new A().getInnerA();
}
連結到外部類
當生成一個內部類的物件時,次物件與製造它的外圍物件之間就有了一種聯絡,所有它能訪問其外圍物件的所有成員,而不需要任何特殊條件。此外,內部類還擁有其外圍類的所有元素的訪問權。 這種能力是如何做到的:當某個外圍類的物件建立了一個內部類物件時,次內部類物件必定會祕密的捕獲一個指向那個外圍類物件的引用。然後,在訪問此外圍類的成員時,就是用那個引用來選擇外圍類的成員。但是要注意,只有內部類的物件與其外圍類的物件相關聯的情況下才能被建立(也就是說 ,內部類是非 static 類時)。構建內部類物件時,需要一個指向其外圍類物件的引用,如果編譯器訪問不到就會報錯。
使用 .this 與 .new
如果需要生成對外部類物件的引用,可以使用外部類的名字後面緊跟圓點和 this 。這樣產生的引用自動地具有正確的型別,這一點在編譯器就被知曉並受到檢查,因此沒有任何執行時開銷。
public class A {
class B {
A getA() {
return A.this;
}
}
}
如果想直接建立內部類物件,就需要使用外部類的物件來建立內部類物件。而不能宣告 a.new A.b() ;
class A {
// .......
public static void main(String[] args ) {
A a = new A();
A.B b = a.new B();
}
}
在擁有外部類物件之前是不可能建立內部類物件的。這是因為內部類物件會暗暗地連線到建立它的內部類物件上。但是,如果建立的是巢狀類(靜態內部類),那麼他就不需要對外部類物件的引用。
內部類與向上轉型
當將內部類向上轉型為其基類,尤其是轉型為一個介面的時候,內部類就有了用武之地。(從實現了某個介面的物件,得到對此物件的引用,與向上轉型為這個物件的基類,實質上效果是一樣的。)這是因為此內部類——某個介面的實現——能夠完全不可見,並且不可用。所得到的只是指向基類或介面的引用,所以能夠很方便地隱藏實現細節。 簡單地說明這種實現
- 建立公共介面 A 。
- 在某一個類 B 中新增一個 private(或 protected )的 A 介面的實現類 AImpl 。
- 在外部類提供一個 public 介面來返回一個 A 的引用。
優點:
- 除了 A 這個外圍類,其它的類都不能訪問或修改 AImpl。
- 通過公共介面取得的 A 的引用,由於無法訪問到 B 中實現的 AImpl 名字,所以無法向下轉型( 或protected 內部類,除非是繼承自它的子類)。
在方法和作用域內的內部類
為什麼要在一個方法裡面或者在任意的作用域內定義內部類,理由有二:
- 實現了某型別的介面,於是可以建立並返回對其的引用。
- 希望建立一個不是公共可用的類來解決你的複雜的問題。
以下是一些可以放內部類的位置
- 一個定義在方法中的類。
- 一個定義在作用域中的類,次作用域在方法的內部。
- 一個實現了介面的匿名類。
- 一個匿名類,擴充套件了有非預設構造器的類。
- 一個匿名類,它執行欄位初始化。
- 一個匿名類,它通過例項初始化實現構造(匿名類不可能有構造器)。
一、展示在方法的作用域內建立一個完整的類。稱作區域性內部類
public class D {
public A getA() {
class AImpl implements A {
//......
}
return new AImpl();
}
//.......
}
AImpl 類是方法的一部分,而不是 D 的一部分,,所以,在方法之外不能訪問 AImpl 。雖然 AImpl 在方法中,但這並不意味方法執行完畢, AImpl 這個物件就不能用了。
在同一子類目下的任意類中對某個內部類使用類識別符號 AImpl ,這並不會有命名衝突。 二、展示在任意作用域內嵌入一個內部類:
public class A {
private void f(boolean b) {
if( b ) {
class B {
String getSlip() {
return "1";
}
//....
}
B b = new B();
String str = b.getSlip();
}
}
}
雖然這個類是放在 if 語句中,但是並不意味著建立需要條件,它其實與別的類一起編譯過了。但是,在定義這個類的作用域之外,它是不可用的;初次之外,它與普通的類一樣。
匿名內部類
public class B {
public A newA() {
return new A() {
private int i = 11;
};
}
//.....
}
newA() 方法將返回值的生成與表示這個返回值的類的定義結合在一起。另外這個類是匿名的。 表示的是:建立一個繼承自 A 的匿名類的物件。通過 new 表示式返回的引用被自動向上轉型為對 A 的引用。 在這個匿名類中,使用了預設的構造器來生成 A 。下面展示,如果基類需要帶參構造器該怎麼辦:(注意這裡區別於匿名類需要帶參構造器)
public class B {
public A newA(int x) {
return new A(x) {
private int i = 11;
public int value() {
return super.value();
}
};
}
//.....
}
只需要簡單地傳遞合適的引數給基類的構造器即可。
如果定義一個匿名內部類,並且希望它使用一個在其外部定義的物件,那麼編譯器會要求其引數引用是 final 的。
如果需要構造器的行為,因為匿名類中不可能有命名構造器(因為它根本沒有名字!),但通過例項初始化,就能夠達到為匿名累不累建立一個構造器的效果,like this:
abstract class Base {
public Base(int i) {
//......
}
}
public class A {
public static Vase getBase(int i) {
return new Base(i) {
{
public void f() {};
//.....
}
};
}
在這個例子中,不要求變數 i 一定是 final 的。因為 i 被傳遞給匿名類的基類的構造器,它並不會在匿名類內部被直接使用。 如果在匿名類內部使用,則要求傳入的引數是 final 的。 當然它收到了限制——不能過載例項初始化方式,所以僅有一個這樣的構造器。 匿名內部類與正規的繼承相比有些受限,因為匿名內部類既可以擴充套件類,也可以實現介面,但是不能兩者兼備,而且如果是實現介面,也只能實現一個介面。
巢狀類
如果不需要內部類物件與其外圍類之間有聯絡,那麼可以將內部類宣告為 static 。這通常成為巢狀類。 普通的內部類物件隱式地儲存了一個引用,指向建立它的外圍類物件,然而,當內部類是 static 的時,就不是這樣
- 要建立巢狀類的物件,並不需要其外圍類的物件。
- 不能從巢狀類的物件中訪問非靜態的外圍類物件。
還有一個區別就是,普通的內部類的欄位與方法,只能放在類的外部層次上,所以普通的內部類不能有 static 資料和 static 欄位,也不能包含巢狀類。但是巢狀類可以包含所有這些東西。