【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 標準,與平臺對‘\)’符號的設置沒有關系。
【Java編程思想】10.內部類