【Java程式設計思想】10.內部類
將一個類的定義放在另一個類的定義內部,這就是內部類。
10.1 建立內部類
- 內部類的名字是巢狀在外部類裡面的
- 外部類可以有方法,返回一個指向內部類的呼叫。(外部類中可以呼叫內部類)
- 如果在外部類中,希望能在除了靜態方法之外的任意位置建立某個內部類物件,那麼可以向下面這樣指明物件型別。
OuterClassName.InnerClassName x = new InnerClassName();
10.2 連結到外部類
在建立了一個內部類的物件後,內部類與製造它的外圍物件(enclosing object,其實就是其所屬於的外部類)之間就有了一種聯絡。這時內部類可以訪問外圍物件的所有成員,且不需要任何特殊條件
換一種說法,就是內部類可以訪問外圍類的方法和欄位,就好像自己擁有這些方法和欄位一樣。
內部類擁有外圍類所有成員的訪問權的原因:
- 某個外圍類的物件建立了一個內部類物件。
- 內部類物件會祕密捕獲一個指向外圍類物件的引用。
- 內部類訪問外圍類成員時,實際使用的就是這個引用。
- 因此構建內部類物件時,需要一個指向外圍類物件的引用,如果編譯器找不到該引用就會報錯
回到上一章節提到的 tips,為什麼內部類的物件只能與外圍類的物件相關聯的情況下才能被建立(在內部類是非靜態類時),為什麼在外部類中的靜態方法建立內部類物件時需要
OuterClassName.InnerClassName
這樣的宣告。也是因為靜態區本身是獨立的,
10.3 使用 .this 和 .new
如果需要生成對外部類物件的引用,可以使用 OuterClassName.this
的形式。這樣產生的引用自動地具有正確的型別(這一點在編譯期就會被確認,因此節省了執行時開銷)。
如果需要讓外圍類去建立其某個內部類的物件,可在 new 表示式中提供對其他外部類物件的引用,需要 .new
語法,如下:
OuterClassName x = new OuterClassName();
OuterClassName.InnerClassName y = x.new InnerClassName();
也就是說,想直接建立對內部類物件,必須使用外部類的物件來建立內部類的物件。因此,也可以說,在擁有外部類物件之前是不可能建立內部類物件的(原因見上一章)。
但是如果建立的是巢狀類(靜態內部類),就不需要對外部類物件的引用。
10.4 內部類與向上轉型
當將內部類向上轉型為基類,尤其是轉型為一個介面的時候,內部類-->某個介面的實現-->可以完全不可見,並且不可用。所能得到的只是指向基類或者介面的引用,這樣就將實現細節隱藏起來了。
實現某個介面的物件,從而得到該介面的引用=將內部類向上轉型為基類
使用內部類去繼承類,或是實現介面,可以很好的阻止外部的訪問,隱藏實現細節,阻止任何依賴於型別的編碼。
10.5 在方法和作用域內的內部類
可以在一個方法裡,或者任意的作用域內定義內部類,原因如下:
- 實現了某個型別的介面,就可以建立並返回對其的應用。
- 要解決複雜的問題,需要建立一個類輔助,但是不希望這個類是公共可用的。
內部類還有其他使用方式,包括:
- 一個定義在方法中的類。
在方法的作用域內(而不是其他類的作用域內)建立一個完整的類,被稱為區域性內部類。 - 一個定義在作用域內的類,此作用域在方法內部。
像這類內部類,僅能作用在對應的作用域之內,除此之外與普通類一致。 - 一個實現了介面的匿名類。
- 一個匿名類,拓展了有非預設構造器的類。
- 一個匿名類,執行欄位初始化。
- 一個匿名類,通過例項初始化實現構造(匿名類不能有構造器)。
10.6 匿名內部類
建立一個實現了介面的內部類:
public Contents contents() {
return new Contents() { // Insert a class definition
private int i = 11;
@Override
public int value() { return i; }
}; // Semicolon required in this case
}
建立一個使用有引數構造器的基類的匿名內部類:
public Wrapping wrapping(int x) {
// Base constructor call:
return new Wrapping(x) { // Pass constructor argument.
@Override
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
匿名內部類末尾的分號,並不是用來標記次內部類結束的。實際上他標記的是表示式的結束,只不過表示式正好包含內部類而已。
建立執行欄位初始化的匿名內部類:
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
@Override
public String readLabel() {
return label;
}
};
}
在匿名內部類中,使用一個在其外部定義的物件時,編譯器會要求其引數引用時 final,就跟 Java8中 lambda 表示式中的引用外部引數一樣。否則編譯會報錯。
建立通過例項初始化實現構造器效果的匿名內部類:
public Destination destination(final String dest, final float price) {
return new Destination() {
private int cost;
// Instance initialization for each object:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
@Override
public String readLabel() {
return label;
}
};
}
對於匿名類而言,例項初始化的實際效果就是構造器(當然是受到了限制-->不能過載例項初始化方法,僅僅是擁有這樣一個勾構造器)
匿名內部類既可以繼承拓展類,也可以實現介面(只能實現一個介面),但是不能兩者兼備。
有了內部類,可以嘗試再次實現第九章中的工廠方法:
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
private Implementation1() {}
@Override
public void method1() {print("Implementation1 method1");}
@Override
public void method2() {print("Implementation1 method2");}
public static ServiceFactory factory = new ServiceFactory() {
public Service getService() {
return new Implementation1();
}
};
}
class Implementation2 implements Service {
private Implementation2() {}
@Override
public void method1() {print("Implementation2 method1");}
@Override
public void method2() {print("Implementation2 method2");}
public static ServiceFactory factory = new ServiceFactory() {
public Service getService() {
return new Implementation2();
}
};
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
// Implementations are completely interchangeable:
serviceConsumer(Implementation2.factory);
}
}
與之前的工廠方法相比,用於 Implementation1
和 Implementation2
的構造器都可以是 private 的,並且沒有任何必要去建立作為工廠的實現類。另外從來只需要單一的工廠物件。
10.7 巢狀類
如果不需要內部類物件與外圍類之間有聯絡,則可以將內部類宣告為 static,這就是巢狀類,
對於普通內部類:
- 普通內部類物件隱式的儲存了其外圍類物件的引用
- 普通內部類的欄位與方法,只能放在類的外部層次上,因此普通內部類不能有 static 資料和欄位,也不能包含巢狀類。
而巢狀類:
- 要建立巢狀類的物件,並不需要其外圍類物件。
- 不能從巢狀類的物件中訪問非靜態的外圍類物件。
- 巢狀類可以包含 static 資料和欄位,
關於巢狀類還有如下幾種使用方式:
- 介面內部的巢狀類:
正常情況下不能在介面內部放置任何實現程式碼,但是巢狀類可以作為介面的一部分(放在介面中的任何類都自動式 public 和 static 的),甚至可以在巢狀內部類中實現外圍介面。 - 從多層巢狀類中訪問外部類的成員:
一個內部類被巢狀多少次不重要-->這個內部類可以透明的訪問所有它所嵌入的外圍類的所有成員。
10.8 為什麼需要內部類
內部類實現一個介面與外圍類實現一個介面的區別在於:後者不是總能享用到介面帶來的方便,有時需要用到介面的實現。
所以可以得出一個結論:每個內部類都能獨立的繼承自一個(介面的)實現,所以無論外圍類是否已經繼承了某個(介面的)實現,對於內部類都沒有影響。這是內部類最吸引人的特性!
這段其實就是說,無論外圍類怎麼搞怎麼玩,我都能用外圍類裡面的內部類額外的單獨去實現一個特定的介面。這個特性在外圍類已經繼承抽象類或具體類的時候,去實現多重繼承時特別有用。
內部類的一些額外特性:
- 內部類可以有多個例項,每個例項都有自己的狀態資訊,並且與其外圍類物件的資訊相互獨立。
- 在單個外圍類中,可以讓多個內部類以不同的方式實現同一個介面,或繼承同一個類。
- 建立內部類物件的時刻並不依賴於外圍類物件的建立(
那外圍類的引用咋整呢。。。建立內部類只需要外圍類物件的引用,是引用就成) - 內部類並沒有“is-a”關係,它只是一二個獨立的個體。
關於閉包與回撥
閉包(closure)是一個可呼叫的物件,它記錄了一些資訊,這些資訊來自於建立它的作用域。
按照這樣的定義,可以說內部類是面向物件的閉包(Java 並沒有顯式的支援閉包),因為它不僅包含外圍類獨享的資訊,還自動擁有一個指向此外圍類物件的引用。
通過內部類可以實現類似其他語言的指標機制帶來的回撥功能,通過回撥,物件可以攜帶一些資訊,這些資訊允許該物件在稍後的某個時候呼叫初始的物件。
我們可以簡單的把閉包理解為“一塊程式碼可以傳入另一個地方,並且在終點處可以執行該程式碼”,用 Java 語言來描述就是“可以把一個類物件打包傳給另一個類物件裡。
interface Incrementable {
void increment();
}
class MyIncrement {
public void increment() {
print("Other operation");
}
static void f(MyIncrement mi) {
mi.increment();
}
}
class Callee2 extends MyIncrement {
private int i = 0;
public void increment() {
super.increment();
i++;
print(i);
}
private class Closure implements Incrementable {
public void increment() {
// Specify outer-class method, otherwise you'd get an infinite recursion:
Callee2.this.increment();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}
class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) {
callbackReference = cbh;
}
void go() {
callbackReference.increment();
}
}
public class Callbacks {
public static void main(String[] args) {
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller2 = new Caller(c2.getCallbackReference()); // 展示回撥
caller2.go();
caller2.go();
}
}
輸出:
Other operation
1
Other operation
2
Other operation
3
Callee2
繼承了 MyIncrement
,就不能為了 Incrementable
的用途而覆蓋 increment()
方法,於是使用內部類獨立實現 Incrementable
介面的方法。這麼做的同時沒有修改外圍類的介面。
內部類 Closure
實現了 Incrementable
,以提供一個返回 Callee2
的“鉤子”(hook)。且這個鉤子返回制定了規則:無論誰獲得 Incrementable
的引用,都只能呼叫 increment()
方法,除此之外沒有其他功能。
Caller
的構造器需要一個 Incrementable
的引用做引數,然後在以後的某個時候,Caller
物件可以使用此引用回撥 Callee
類。
回撥其實就是,A類 呼叫 B類 中的方法 b,然後 B 類反過來呼叫 A 類中的方法 a,那麼方法 a 就是回撥方法。具體實現上各有差異,一般都用在像執行緒啊,訊息處理這塊。回撥的價值就在於,可以在執行時動態的決定需要呼叫什麼方法。
之所以在內部類這部分提到回撥,就是因為 Java 這種仿閉包的非靜態內部類(記錄外部類的詳細資訊;保留外部類物件的引用;可以直接呼叫外部類任意成員),可以很方便的實現回撥功能--->在某個方法獲得內部類物件的引用後,反過來直接呼叫外圍類的方法,這也是回撥的表現形式。
內部類與控制框架
應用程式框架(application framework)就是被設計泳衣解決某些特定問題的一個類或一組類。
控制框架是一類特殊的應用程式框架,用來解決響應事件的需求。主要用來響應事件的系統被稱作事件驅動系統。
在這類設計中,關鍵的點在於需要“使變化的事務和不變的事物相互分離”。
內部類允許:
- 控制框架的完整實現是由單個的類建立的,從而使實現的細節被封裝了起來。內部類用來表示解決問題所必需的各種不同的
action()
。 - 內部類能夠很容易的訪問外圍類成員。
10.9 內部類的繼承
在繼承內部類的時候,因為內部類的構造器必須連結到指向其外圍類物件的引用,這個“祕密的”引用必須被初始化,在匯出類中也不會再存在可連線的預設物件,因此在繼承時需要使用下面的語法,描述清楚匯出類,基類(內部類),外圍類之間的關係。
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
可以看到,在生成一個構造器的時候,不能使用預設構造器(編譯器會報錯),需要在構造器內部使用 enclosingClassReference.super()
,提供必要的內部類引用。
10.10 內部類可以被覆蓋嗎
對於內部類來說,“覆蓋”它就好像它是外圍類的一個方法,但是並沒有起到什麼作用。
這種情況發生後,其實內部類和“覆蓋”的內部類完全是兩個獨立的實體,各自在自己的名稱空間內。
但是如果在繼承內部類時,指定內部類的外圍類物件的引用,那麼就會明確繼承的類,這樣就跟正常的覆蓋一樣,重新實現對應方法即可。
10.11 區域性內部類
區域性內部類(例如在方法體內建立的類等)不能有訪問說明符,因為他不是外圍類的一部分。但是它可以訪問當前程式碼塊內的常量,以及此外圍類的所有成員。
在實現上,區域性內部類和匿名內部類是相似的,二者具有相同的行為和能力。
- 但是區域性內部類是有名稱的,所以可以有“帶有命名”的構造器,也可以過載構造器。
- 而匿名內部類只能用於例項初始化。
- 同時使用區域性內部類的時候,可以建立不止一個該內部類的物件。而匿名內部類只能在例項初始化的時候被建立一次。
10.12 內部類識別符號
每個類都會產生一個 .class
檔案,其中包含了如何建立該型別的物件的全部資訊(此資訊產生一個 meta-class,叫做 Class 物件。
因此內部類也必須生成一個 .class
檔案,以包含他們的 Class 物件資訊。這些類檔案的命名有嚴格的規則,必須是外圍類的名字。加上‘\(’,再加上內部類的名字構成。 ```java OuterClassName\)InnerClassName.class
```
如果是匿名內部類,編譯器會簡單的產生一個數字作為其識別符號。
如果是巢狀內部類,只需直接將他們的名字加在其外圍識別符號與‘\(’後面。 這種命名方式是純粹的 Java 標準,與平臺對‘\)’符號的設定沒有關係。