# 7.6 內部類
在Java 1.1中,可將一個類定義置入另一個類定義中。這就叫作“內部類”。內部類對我們非常有用,因為利用它可對那些邏輯上相互聯系的類進行分組,并可控制一個類在另一個類里的“可見性”。然而,我們必須認識到內部類與以前講述的“組合”方法存在著根本的區別。
通常,對內部類的需要并不是特別明顯的,至少不會立即感覺到自己需要使用內部類。在本章的末尾,介紹完內部類的所有語法之后,大家會發現一個特別的例子。通過它應該可以清晰地認識到內部類的好處。
創建內部類的過程是平淡無奇的:將類定義置入一個用于封裝它的類內部(若執行這個程序遇到麻煩,請參見第3章的3.1.2小節“賦值”):
```
//: Parcel1.java
// Creating inner classes
package c07.parcel1;
public class Parcel1 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
// Using inner classes looks just like
// using any other class, within Parcel1:
public void ship(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship("Tanzania");
}
} ///:~
```
若在`ship()`內部使用,內部類的使用看起來和其他任何類都沒什么分別。在這里,唯一明顯的區別就是它的名字嵌套在`Parcel1`里面。但大家不久就會知道,這其實并非唯一的區別。
更典型的一種情況是,一個外部類擁有一個特殊的方法,它會返回指向一個內部類的引用。就象下面這樣:
```
//: Parcel2.java
// Returning a handle to an inner class
package c07.parcel2;
public class Parcel2 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
public Destination to(String s) {
return new Destination(s);
}
public Contents cont() {
return new Contents();
}
public void ship(String dest) {
Contents c = cont();
Destination d = to(dest);
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p.ship("Tanzania");
Parcel2 q = new Parcel2();
// Defining handles to inner classes:
Parcel2.Contents c = q.cont();
Parcel2.Destination d = q.to("Borneo");
}
} ///:~
```
若想在除外部類非`static`方法內部之外的任何地方生成內部類的一個對象,必須將那個對象的類型設為`外部類名.內部類名`,就象`main()`中展示的那樣。
## 7.6.1 內部類和向上轉換
迄今為止,內部類看起來仍然沒什么特別的地方。畢竟,用它實現隱藏顯得有些大題小做。Java已經有一個非常優秀的隱藏機制——只允許類成為“友好的”(只在一個包內可見),而不是把它創建成一個內部類。
然而,當我們準備向上轉換到一個基類(特別是到一個接口)的時候,內部類就開始發揮其關鍵作用(從用于實現的對象生成一個接口引用具有與向上轉換至一個基類相同的效果)。這是由于內部類隨后可完全進入不可見或不可用狀態——對任何人都將如此。所以我們可以非常方便地隱藏實現細節。我們得到的全部回報就是一個基類或者接口的引用,而且甚至有可能不知道準確的類型。就象下面這樣:
```
//: Parcel3.java
// Returning a handle to an inner class
package c07.parcel3;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel3 {
private class PContents extends Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public Destination dest(String s) {
return new PDestination(s);
}
public Contents cont() {
return new PContents();
}
}
class Test {
public static void main(String[] args) {
Parcel3 p = new Parcel3();
Contents c = p.cont();
Destination d = p.dest("Tanzania");
// Illegal -- can't access private class:
//! Parcel3.PContents c = p.new PContents();
}
} ///:~
```
現在,`Contents`和`Destination`代表可由客戶程序員使用的接口(記住接口會將自己的所有成員都變成`public`屬性)。為方便起見,它們置于單獨一個文件里,但原始的`Contents`和`Destination`在它們自己的文件中是相互`public`的。
在`Parcel3`中,一些新東西已經加入:內部類`PContents`被設為`private`,所以除了`Parcel3`之外,其他任何東西都不能訪問它。`PDestination`被設為`protected`,所以除了`Parcel3`,`Parcel3`包內的類(因為`protected`也為包賦予了訪問權;也就是說,`protected`也是“友好的”),以及`Parcel3`的繼承者之外,其他任何東西都不能訪問`PDestination`。這意味著客戶程序員對這些成員的認識與訪問將會受到限制。事實上,我們甚至不能向下轉換到一個`private`內部類(或者一個`protected`內部類,除非自己本身便是一個繼承者),因為我們不能訪問名字,就象在`classTest`里看到的那樣。所以,利用`private`內部類,類設計人員可完全禁止其他人依賴類型編碼,并可將具體的實現細節完全隱藏起來。除此以外,從客戶程序員的角度來看,一個接口的范圍沒有意義的,因為他們不能訪問不屬于公共接口類的任何額外方法。這樣一來,Java編譯器也有機會生成效率更高的代碼。
普通(非內部)類不可設為`private`或`protected`——只允許public或者“友好的”。
注意`Contents`不必成為一個抽象類。在這兒也可以使用一個普通類,但這種設計最典型的起點依然是一個“接口”。
## 7.6.2 方法和作用域中的內部類
至此,我們已基本理解了內部類的典型用途。對那些涉及內部類的代碼,通常表達的都是“單純”的內部類,非常簡單,且極易理解。然而,內部類的設計非常全面,不可避免地會遇到它們的其他大量用法——假若我們在一個方法甚至一個任意的作用域內創建內部類。有兩方面的原因促使我們這樣做:
(1) 正如前面展示的那樣,我們準備實現某種形式的接口,使自己能創建和返回一個引用。
(2) 要解決一個復雜的問題,并希望創建一個類,用來輔助自己的程序方案。同時不愿意把它公開。
在下面這個例子里,將修改前面的代碼,以便使用:
(1) 在一個方法內定義的類
(2) 在方法的一個作用域內定義的類
(3) 一個匿名類,用于實現一個接口
(4) 一個匿名類,用于擴展擁有非默認構造器的一個類
(5) 一個匿名類,用于執行字段初始化
(6) 一個匿名類,通過實例初始化進行構建(匿名內部類不可擁有構造器)
所有這些都在`innerscopes`包內發生。首先,來自前述代碼的通用接口會在它們自己的文件里獲得定義,使它們能在所有的例子里使用:
```
//: Destination.java
package c07.innerscopes;
interface Destination {
String readLabel();
} ///:~
```
由于我們已認為`Contents`可能是一個抽象類,所以可采取下面這種更自然的形式,就象一個接口那樣:
```
//: Contents.java
package c07.innerscopes;
interface Contents {
int value();
} ///:~
```
盡管是含有具體實現細節的一個普通類,但`Wrapping`也作為它所有派生類的一個通用“接口”使用:
```
//: Wrapping.java
package c07.innerscopes;
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
} ///:~
```
在上面的代碼中,我們注意到`Wrapping`有一個要求使用參數的構造器,這就使情況變得更加有趣了。
第一個例子展示了如何在一個方法的作用域(而不是另一個類的作用域)中創建一個完整的類:
```
//: Parcel4.java
// Nesting a class within a method
package c07.innerscopes;
public class Parcel4 {
public Destination dest(String s) {
class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Destination d = p.dest("Tanzania");
}
} ///:~
```
`PDestination`類屬于`dest()`的一部分,而不是`Parcel4`的一部分(同時注意可為相同目錄內每個類內部的一個內部類使用類標識符`PDestination`,這樣做不會發生命名的沖突)。因此,`PDestination`不可從`dest()`的外部訪問。請注意在返回語句中發生的向上轉換——除了指向基類`Destination`的一個引用之外,沒有任何東西超出`dest()`的邊界之外。當然,不能由于類`PDestination`的名字置于`dest()`內部,就認為在`dest()`返回之后`PDestination`不是一個有效的對象。
下面這個例子展示了如何在任意作用域內嵌套一個內部類:
```
//: Parcel5.java
// Nesting a class within a scope
package c07.innerscopes;
public class Parcel5 {
private void internalTracking(boolean b) {
if(b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
// Can't use it here! Out of scope:
//! TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel5 p = new Parcel5();
p.track();
}
} ///:~
```
`TrackingSlip`類嵌套于一個`if`語句的作用域內。這并不意味著類是有條件創建的——它會隨同其他所有東西得到編譯。然而,在定義它的那個作用域之外,它是不可使用的。除這些以外,它看起來和一個普通類并沒有什么區別。
下面這個例子看起來有些奇怪:
```
//: Parcel6.java
// A method that returns an anonymous inner class
package c07.innerscopes;
public class Parcel6 {
public Contents cont() {
return new Contents() {
private int i = 11;
public int value() { return i; }
}; // Semicolon required in this case
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
Contents c = p.cont();
}
} ///:~
```
`cont()`方法同時合并了返回值的創建代碼,以及用于表示那個返回值的類。除此以外,這個類是匿名的——它沒有名字。而且看起來似乎更讓人摸不著頭腦的是,我們準備創建一個`Contents`對象:
```
return new Contents()
```
但在這之后,在遇到分號之前,我們又說:“等一等,讓我先在一個類定義里再耍一下花招”:
```
return new Contents() {
private int i = 11;
public int value() { return i; }
};
```
這種奇怪的語法要表達的意思是:“創建從`Contents`派生出來的匿名類的一個對象”。由`new`表達式返回的引用會自動向上轉換成一個`Contents`引用。匿名內部類的語法其實要表達的是:
```
class MyContents extends Contents {
private int i = 11;
public int value() { return i; }
}
return new MyContents();
```
在匿名內部類中,`Contents`是用一個默認構造器創建的。下面這段代碼展示了基類需要含有參數的一個構造器時做的事情:
```
//: Parcel7.java
// An anonymous inner class that calls the
// base-class constructor
package c07.innerscopes;
public class Parcel7 {
public Wrapping wrap(int x) {
// Base constructor call:
return new Wrapping(x) {
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Wrapping w = p.wrap(10);
}
} ///:~
```
也就是說,我們將適當的參數簡單地傳遞給基類構造器,在這兒表現為在`new Wrapping(x)`中傳遞`x`。匿名類不能擁有一個構造器,這和在調用`super()`時的常規做法不同。
在前述的兩個例子中,分號并不標志著類主體的結束(和C++不同)。相反,它標志著用于包含匿名類的那個表達式的結束。因此,它完全等價于在其他任何地方使用分號。
若想對匿名內部類的一個對象進行某種形式的初始化,此時會出現什么情況呢?由于它是匿名的,沒有名字賦給構造器,所以我們不能擁有一個構造器。然而,我們可在定義自己的字段時進行初始化:
```
//: Parcel8.java
// An anonymous inner class that performs
// initialization. A briefer version
// of Parcel5.java.
package c07.innerscopes;
public class Parcel8 {
// Argument must be final to use inside
// anonymous inner class:
public Destination dest(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Destination d = p.dest("Tanzania");
}
} ///:~
```
若試圖定義一個匿名內部類,并想使用在匿名內部類外部定義的一個對象,則編譯器要求外部對象為`final`屬性。這正是我們將`dest()`的參數設為`final`的原因。如果忘記這樣做,就會得到一條編譯期出錯提示。
只要自己只是想分配一個字段,上述方法就肯定可行。但假如需要采取一些類似于構造器的行動,又應怎樣操作呢?通過Java 1.1的實例初始化,我們可以有效地為一個匿名內部類創建一個構造器:
```
//: Parcel9.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
package c07.innerscopes;
public class Parcel9 {
public Destination
dest(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;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.dest("Tanzania", 101.395F);
}
} ///:~
```
在實例初始化模塊中,我們可看到代碼不能作為類初始化模塊(即`if`語句)的一部分執行。所以實際上,一個實例初始化模塊就是一個匿名內部類的構造器。當然,它的功能是有限的;我們不能對實例初始化模塊進行重載處理,所以只能擁有這些構造器的其中一個。
## 7.6.3 鏈接到外部類
迄今為止,我們見到的內部類好象僅僅是一種名字隱藏以及代碼組織方案。盡管這些功能非常有用,但似乎并不特別引人注目。然而,我們還忽略了另一個重要的事實。創建自己的內部類時,那個類的對象同時擁有指向封裝對象(這些對象封裝或生成了內部類)的一個鏈接。所以它們能訪問那個封裝對象的成員——毋需取得任何資格。除此以外,內部類擁有對封裝類所有元素的訪問權限(注釋②)。下面這個例子闡示了這個問題:
```
//: Sequence.java
// Holds a sequence of Objects
interface Selector {
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size) {
o = new Object[size];
}
public void add(Object x) {
if(next < o.length) {
o[next] = x;
next++;
}
}
private class SSelector implements Selector {
int i = 0;
public boolean end() {
return i == o.length;
}
public Object current() {
return o[i];
}
public void next() {
if(i < o.length) i++;
}
}
public Selector getSelector() {
return new SSelector();
}
public static void main(String[] args) {
Sequence s = new Sequence(10);
for(int i = 0; i < 10; i++)
s.add(Integer.toString(i));
Selector sl = s.getSelector();
while(!sl.end()) {
System.out.println((String)sl.current());
sl.next();
}
}
} ///:~
```
②:這與C++“嵌套類”的設計頗有不同,后者只是一種單純的名字隱藏機制。在C++中,沒有指向一個封裝對象的鏈接,也不存在默認的訪問權限。
其中,`Sequence`只是一個大小固定的對象數組,有一個類將其封裝在內部。我們調用`add()`,以便將一個新對象添加到`Sequence`末尾(如果還有地方的話)。為了取得`Sequence`中的每一個對象,要使用一個名為`Selector`的接口,它使我們能夠知道自己是否位于最末尾(`end()`),能觀看當前對象(`current() Object`),以及能夠移至`Sequence`內的下一個對象(`next() Object`)。由于`Selector`是一個接口,所以其他許多類都能用它們自己的方式實現接口,而且許多方法都能將接口作為一個參數使用,從而創建一般的代碼。
在這里,`SSelector`是一個私有類,它提供了`Selector`功能。在`main()`中,大家可看到`Sequence`的創建過程,在它后面是一系列字符串對象的添加。隨后,通過對`getSelector()`的一個調用生成一個`Selector`。并用它在`Sequence`中移動,同時選擇每一個項目。
從表面看,`SSelector`似乎只是另一個內部類。但不要被表面現象迷惑。請注意觀察`end()`,`current()`以及`next()`,它們每個方法都引用了`o`。`o`是個不屬于`SSelector`一部分的引用,而是位于封裝類里的一個`private`字段。然而,內部類可以從封裝類訪問方法與字段,就象已經擁有了它們一樣。這一特征對我們來說是非常方便的,就象在上面的例子中看到的那樣。
因此,我們現在知道一個內部類可以訪問封裝類的成員。這是如何實現的呢?內部類必須擁有對封裝類的特定對象的一個引用,而封裝類的作用就是創建這個內部類。隨后,當我們引用封裝類的一個成員時,就利用那個(隱藏)的引用來選擇那個成員。幸運的是,編譯器會幫助我們照管所有這些細節。但我們現在也可以理解內部類的一個對象只能與封裝類的一個對象聯合創建。在這個創建過程中,要求對封裝類對象的引用進行初始化。若不能訪問那個引用,編譯器就會報錯。進行所有這些操作的時候,大多數時候都不要求程序員的任何介入。
## 7.6.4 `static`內部類
為正確理解`static`在應用于內部類時的含義,必須記住內部類的對象默認持有創建它的那個封裝類的一個對象的引用。然而,假如我們說一個內部類是`static`的,這種說法卻是不成立的。`static`內部類意味著:
(1) 為創建一個`static`內部類的對象,我們不需要一個外部類對象。
(2) 不能從`static`內部類的一個對象中訪問一個外部類對象。
但在存在一些限制:由于`static`成員只能位于一個類的外部級別,所以內部類不可擁有`static`數據或`static`內部類。
倘若為了創建內部類的對象而不需要創建外部類的一個對象,那么可將所有東西都設為`static`。為了能正常工作,同時也必須將內部類設為`static`。如下所示:
```
//: Parcel10.java
// Static inner classes
package c07.parcel10;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel10 {
private static class PContents
extends Contents {
private int i = 11;
public int value() { return i; }
}
protected static class PDestination
implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public static Destination dest(String s) {
return new PDestination(s);
}
public static Contents cont() {
return new PContents();
}
public static void main(String[] args) {
Contents c = cont();
Destination d = dest("Tanzania");
}
} ///:~
```
在`main()`中,我們不需要`Parcel10`的對象;相反,我們用常規的語法來選擇一個`static`成員,以便調用將引用返回`Contents`和`Destination`的方法。
通常,我們不在一個接口里設置任何代碼,但`static`內部類可以成為接口的一部分。由于類是“靜態”的,所以它不會違反接口的規則——`static`內部類只位于接口的命名空間內部:
```
//: IInterface.java
// Static inner classes inside interfaces
interface IInterface {
static class Inner {
int i, j, k;
public Inner() {}
void f() {}
}
} ///:~
```
在本書早些時候,我建議大家在每個類里都設置一個`main()`,將其作為那個類的測試床使用。這樣做的一個缺點就是額外代碼的數量太多。若不愿如此,可考慮用一個`static`內部類容納自己的測試代碼。如下所示:
```
//: TestBed.java
// Putting test code in a static inner class
class TestBed {
TestBed() {}
void f() { System.out.println("f()"); }
public static class Tester {
public static void main(String[] args) {
TestBed t = new TestBed();
t.f();
}
}
} ///:~
```
這樣便生成一個獨立的、名為`TestBed$Tester`的類(為運行程序,請使用`java TestBed$Tester`命令)。可將這個類用于測試,但不需在自己的最終發行版本中包含它。
## 7.6.5 引用外部類對象
若想生成外部類對象的引用,就要用一個點號以及一個`this`來命名外部類。舉個例子來說,在`Sequence.SSelector`類中,它的所有方法都能產生外部類`Sequence`的存儲引用,方法是采用`Sequence.this`的形式。結果獲得的引用會自動具備正確的類型(這會在編譯期間檢查并核實,所以不會出現運行期的開銷)。
有些時候,我們想告訴其他某些對象創建它某個內部類的一個對象。為達到這個目的,必須在`new`表達式中提供指向其他外部類對象的一個引用,就象下面這樣:
```
//: Parcel11.java
// Creating inner classes
package c07.parcel11;
public class Parcel11 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
public static void main(String[] args) {
Parcel11 p = new Parcel11();
// Must use instance of outer class
// to create an instances of the inner class:
Parcel11.Contents c = p.new Contents();
Parcel11.Destination d =
p.new Destination("Tanzania");
}
} ///:~
```
為直接創建內部類的一個對象,不能象大家或許猜想的那樣——采用相同的形式,并引用外部類名`Parcel11`。此時,必須利用外部類的一個對象生成內部類的一個對象:
```
Parcel11.Contents c = p.new Contents();
```
因此,除非已擁有外部類的一個對象,否則不可能創建內部類的一個對象。這是由于內部類的對象已同創建它的外部類的對象“默默”地連接到一起。然而,如果生成一個`static`內部類,就不需要指向外部類對象的一個引用。
## 7.6.6 從內部類繼承
由于內部類構造器必須同封裝類對象的一個引用聯系到一起,所以從一個內部類繼承的時候,情況會稍微變得有些復雜。這兒的問題是封裝類的“秘密”引用必須獲得初始化,而且在派生類中不再有一個默認的對象可以連接。解決這個問題的辦法是采用一種特殊的語法,明確建立這種關聯:
```
//: InheritInner.java
// Inheriting an inner class
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);
}
} ///:~
```
從中可以看到,`InheritInner`只對內部類進行了擴展,沒有擴展外部類。但在需要創建一個構造器的時候,默認對象已經沒有意義,我們不能只是傳遞封裝對象的一個引用。此外,必須在構造器中采用下述語法:
```
enclosingClassHandle.super();
```
它提供了必要的引用,以便程序正確編譯。
## 7.6.7 內部類可以覆蓋嗎?
若創建一個內部類,然后從封裝類繼承,并重新定義內部類,那么會出現什么情況呢?也就是說,我們有可能覆蓋一個內部類嗎?這看起來似乎是一個非常有用的概念,但“覆蓋”一個內部類——好象它是外部類的另一個方法——這一概念實際不能做任何事情:
```
//: BigEgg.java
// An inner class cannot be overriden
// like a method
class Egg {
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
private Yolk y;
public Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args) {
new BigEgg();
}
} ///:~
```
默認構造器是由編譯器自動組合的,而且會調用基類的默認構造器。大家或許會認為由于準備創建一個`BigEgg`,所以會使用`Yolk`的“被覆蓋”版本。但實際情況并非如此。輸出如下:
```
New Egg()
Egg.Yolk()
```
這個例子簡單地揭示出當我們從外部類繼承的時候,沒有任何額外的內部類繼續下去。然而,仍然有可能“明確”地從內部類繼承:
```
//: BigEgg2.java
// Proper inheritance of an inner class
class Egg2 {
protected class Yolk {
public Yolk() {
System.out.println("Egg2.Yolk()");
}
public void f() {
System.out.println("Egg2.Yolk.f()");
}
}
private Yolk y = new Yolk();
public Egg2() {
System.out.println("New Egg2()");
}
public void insertYolk(Yolk yy) { y = yy; }
public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() {
System.out.println("BigEgg2.Yolk()");
}
public void f() {
System.out.println("BigEgg2.Yolk.f()");
}
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
} ///:~
```
現在,`BigEgg2.Yolk`明確地擴展了`Egg2.Yolk`,而且覆蓋了它的方法。方法`insertYolk()`允許`BigEgg2`將它自己的某個`Yolk`對象向上轉換至`Egg2`的`y`引用。所以當`g()`調用`y.f()`的時候,就會使用`f()`被覆蓋版本。輸出結果如下:
```
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
```
對`Egg2.Yolk()`的第二個調用是`BigEgg2.Yolk`構造器的基類構造器調用。調用
`g()`的時候,可發現使用的是`f()`的被覆蓋版本。
## 7.6.8 內部類標識符
由于每個類都會生成一個`.class`文件,用于容納與如何創建這個類型的對象有關的所有信息(這種信息產生了一個名為`Class`對象的元類),所以大家或許會猜到內部類也必須生成相應的`.class`文件,用來容納與它們的`Class`對象有關的信息。這些文件或類的名字遵守一種嚴格的形式:先是封裝類的名字,再跟隨一個`$`,再跟隨內部類的名字。例如,由`InheritInner.java`創建的`.class`文件包括:
```
InheritInner.class
WithInner$Inner.class
WithInner.class
```
如果內部類是匿名的,那么編譯器會簡單地生成數字,把它們作為內部類標識符使用。若內部類嵌套于其他內部類中,則它們的名字簡單地追加在一個`$`以及外部類標識符的后面。
這種生成內部名稱的方法除了非常簡單和直觀以外,也非常“健壯”,可適應大多數場合的要求(注釋③)。由于它是Java的標準命名機制,所以產生的文件會自動具備“與平臺無關”的能力(注意Java編譯器會根據情況改變內部類,使其在不同的平臺中能正常工作)。
③:但在另一方面,由于`$`也是Unix外殼的一個元字符,所以有時會在列出`.class`文件時遇到麻煩。對一家以Unix為基礎的公司——Sun——來說,采取這種方案顯得有些奇怪。我的猜測是他們根本沒有仔細考慮這方面的問題,而是認為我們會將全部注意力自然地放在源碼文件上。
## 7.6.9 為什么要用內部類:控制框架
到目前為止,大家已接觸了對內部類的運作進行描述的大量語法與概念。但這些并不能真正說明內部類存在的原因。為什么Sun要如此麻煩地在Java 1.1里添加這樣的一種基本語言特性呢?答案就在于我們在這里要學習的“控制框架”。
一個“應用程序框架”是指一個或一系列類,它們專門設計用來解決特定類型的問題。為應用應用程序框架,我們可從一個或多個類繼承,并覆蓋其中的部分方法。我們在覆蓋方法中編寫的代碼用于定制由那些應用程序框架提供的常規方案,以便解決自己的實際問題。“控制框架”屬于應用程序框架的一種特殊類型,受到對事件響應的需要的支配;主要用來響應事件的一個系統叫作“由事件驅動的系統”。在應用程序設計語言中,最重要的問題之一便是“圖形用戶界面”(GUI),它幾乎完全是由事件驅動的。正如大家會在第13章學習的那樣,Java 1.1 AWT屬于一種控制框架,它通過內部類完美地解決了GUI的問題。
為理解內部類如何簡化控制框架的創建與使用,可認為一個控制框架的工作就是在事件“就緒”以后執行它們。盡管“就緒”的意思很多,但在目前這種情況下,我們卻是以計算機時鐘為基礎。隨后,請認識到針對控制框架需要控制的東西,框架內并未包含任何特定的信息。首先,它是一個特殊的接口,描述了所有控制事件。它可以是一個抽象類,而非一個實際的接口。由于默認行為是根據時間控制的,所以部分實現細節可能包括:
```
//: Event.java
// The common methods for any control event
package c07.controller;
abstract public class Event {
private long evtTime;
public Event(long eventTime) {
evtTime = eventTime;
}
public boolean ready() {
return System.currentTimeMillis() >= evtTime;
}
abstract public void action();
abstract public String description();
} ///:~
```
希望`Event`(事件)運行的時候,構造器即簡單地捕獲時間。同時`ready()`告訴我們何時該運行它。當然,`ready()`也可以在一個派生類中被覆蓋,將事件建立在除時間以外的其他東西上。
`action()`是事件就緒后需要調用的方法,而`description()`提供了與事件有關的文字信息。
下面這個文件包含了實際的控制框架,用于管理和觸發事件。第一個類實際只是一個“助手”類,它的職責是容納`Event`對象。可用任何適當的集合替換它。而且通過第8章的學習,大家會知道另一些集合可簡化我們的工作,不需要我們編寫這些額外的代碼:
```
//: Controller.java
// Along with Event, the generic
// framework for all control systems:
package c07.controller;
// This is just a way to hold Event objects.
class EventSet {
private Event[] events = new Event[100];
private int index = 0;
private int next = 0;
public void add(Event e) {
if(index >= events.length)
return; // (In real life, throw exception)
events[index++] = e;
}
public Event getNext() {
boolean looped = false;
int start = next;
do {
next = (next + 1) % events.length;
// See if it has looped to the beginning:
if(start == next) looped = true;
// If it loops past start, the list
// is empty:
if((next == (start + 1) % events.length)
&& looped)
return null;
} while(events[next] == null);
return events[next];
}
public void removeCurrent() {
events[next] = null;
}
}
public class Controller {
private EventSet es = new EventSet();
public void addEvent(Event c) { es.add(c); }
public void run() {
Event e;
while((e = es.getNext()) != null) {
if(e.ready()) {
e.action();
System.out.println(e.description());
es.removeCurrent();
}
}
}
} ///:~
```
`EventSet`可容納100個事件(若在這里使用來自第8章的一個“真實”集合,就不必擔心它的最大尺寸,因為它會根據情況自動改變大小)。`index`(索引)在這里用于跟蹤下一個可用的空間,而`next`(下一個)幫助我們尋找列表中的下一個事件,了解自己是否已經循環到頭。在對`getNext()`的調用中,這一點是至關重要的,因為一旦運行,`Event`對象就會從列表中刪去(使用`removeCurrent()`)。所以`getNext()`會在列表中向前移動時遇到“空洞”。
注意`removeCurrent()`并不只是指示一些標志,指出對象不再使用。相反,它將引用設為`null`。這一點是非常重要的,因為假如垃圾收集器發現一個引用仍在使用,就不會清除對象。若認為自己的引用可能象現在這樣被掛起,那么最好將其設為`null`,使垃圾收集器能夠正常地清除它們。
`Controller`是進行實際工作的地方。它用一個`EventSet`容納自己的`Event`對象,而且`addEvent()`允許我們向這個列表加入新事件。但最重要的方法是`run()`。該方法會在`EventSet`中遍歷,搜索一個準備運行的`Event`對象——`ready()`。對于它發現`ready()`的每一個對象,都會調用`action()`方法,打印出`description()`,然后將事件從列表中刪去。
注意在迄今為止的所有設計中,我們仍然不能準確地知道一個“事件”要做什么。這正是整個設計的關鍵;它怎樣“將發生變化的東西同沒有變化的東西區分開”?或者用我的話來講,“改變的意圖”造成了各類`Event`對象的不同行動。我們通過創建不同的`Event`子類,從而表達出不同的行動。
這里正是內部類大顯身手的地方。它們允許我們做兩件事情:
(1) 在單獨一個類里表達一個控制框架應用的全部實現細節,從而完整地封裝與那個實現有關的所有東西。內部類用于表達多種不同類型的`action()`,它們用于解決實際的問題。除此以外,后續的例子使用了`private`內部類,所以實現細節會完全隱藏起來,可以安全地修改。
(2) 內部類使我們具體的實現變得更加巧妙,因為能方便地訪問外部類的任何成員。若不具備這種能力,代碼看起來就可能沒那么使人舒服,最后不得不尋找其他方法解決。
現在要請大家思考控制框架的一種具體實現方式,它設計用來控制溫室(`Greenhouse`)功能(注釋④)。每個行動都是完全不同的:控制燈光、供水以及溫度自動調節的開與關,控制響鈴,以及重新啟動系統。但控制框架的設計宗旨是將不同的代碼方便地隔離開。對每種類型的行動,都要繼承一個新的`Event`內部類,并在`action()`內編寫相應的控制代碼。
④:由于某些特殊原因,這對我來說是一個經常需要解決的、非常有趣的問題;原來的例子在《C++ Inside & Out》一書里也出現過,但Java提供了一種更令人舒適的解決方案。
作為應用程序框架的一種典型行為,`GreenhouseControls`類是從`Controller`繼承的:
```
//: GreenhouseControls.java
// This produces a specific application of the
// control system, all in a single class. Inner
// classes allow you to encapsulate different
// functionality for each type of event.
package c07.controller;
public class GreenhouseControls
extends Controller {
private boolean light = false;
private boolean water = false;
private String thermostat = "Day";
private class LightOn extends Event {
public LightOn(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here to
// physically turn on the light.
light = true;
}
public String description() {
return "Light is on";
}
}
private class LightOff extends Event {
public LightOff(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here to
// physically turn off the light.
light = false;
}
public String description() {
return "Light is off";
}
}
private class WaterOn extends Event {
public WaterOn(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
water = true;
}
public String description() {
return "Greenhouse water is on";
}
}
private class WaterOff extends Event {
public WaterOff(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
water = false;
}
public String description() {
return "Greenhouse water is off";
}
}
private class ThermostatNight extends Event {
public ThermostatNight(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
thermostat = "Night";
}
public String description() {
return "Thermostat on night setting";
}
}
private class ThermostatDay extends Event {
public ThermostatDay(long eventTime) {
super(eventTime);
}
public void action() {
// Put hardware control code here
thermostat = "Day";
}
public String description() {
return "Thermostat on day setting";
}
}
// An example of an action() that inserts a
// new one of itself into the event list:
private int rings;
private class Bell extends Event {
public Bell(long eventTime) {
super(eventTime);
}
public void action() {
// Ring bell every 2 seconds, rings times:
System.out.println("Bing!");
if(--rings > 0)
addEvent(new Bell(
System.currentTimeMillis() + 2000));
}
public String description() {
return "Ring bell";
}
}
private class Restart extends Event {
public Restart(long eventTime) {
super(eventTime);
}
public void action() {
long tm = System.currentTimeMillis();
// Instead of hard-wiring, you could parse
// configuration information from a text
// file here:
rings = 5;
addEvent(new ThermostatNight(tm));
addEvent(new LightOn(tm + 1000));
addEvent(new LightOff(tm + 2000));
addEvent(new WaterOn(tm + 3000));
addEvent(new WaterOff(tm + 8000));
addEvent(new Bell(tm + 9000));
addEvent(new ThermostatDay(tm + 10000));
// Can even add a Restart object!
addEvent(new Restart(tm + 20000));
}
public String description() {
return "Restarting system";
}
}
public static void main(String[] args) {
GreenhouseControls gc =
new GreenhouseControls();
long tm = System.currentTimeMillis();
gc.addEvent(gc.new Restart(tm));
gc.run();
}
} ///:~
```
注意`light`(燈光)、`water`(供水)、`thermostat`(調溫)以及`rings`都隸屬于外部類`GreenhouseControls`,所以內部類可以毫無阻礙地訪問那些字段。此外,大多數`action()`方法也涉及到某些形式的硬件控制,這通常都要求發出對非Java代碼的調用。
大多數`Event`類看起來都是相似的,但`Bell`(鈴)和`Restart`(重啟)屬于特殊情況。`Bell`會發出響聲,若尚未響鈴足夠的次數,它會在事件列表里添加一個新的`Bell`對象,所以以后會再度響鈴。請注意內部類看起來為什么總是類似于多重繼承:`Bell`擁有`Event`的所有方法,而且也擁有外部類`GreenhouseControls`的所有方法。
`Restart`負責對系統進行初始化,所以會添加所有必要的事件。當然,一種更靈活的做法是避免進行“硬編碼”,而是從一個文件里讀入它們(第10章的一個練習會要求大家修改這個例子,從而達到這個目標)。由于`Restart()`僅僅是另一個`Event`對象,所以也可以在`Restart.action()`里添加一個`Restart`對象,使系統能夠定期重啟。在`main()`中,我們需要做的全部事情就是創建一個`GreenhouseControls`對象,并添加一個`Restart`對象,令其工作起來。
這個例子應該使大家對內部類的價值有一個更加深刻的認識,特別是在一個控制框架里使用它們的時候。此外,在第13章的后半部分,大家還會看到如何巧妙地利用內部類描述一個圖形用戶界面的行為。完成那里的學習后,對內部類的認識將上升到一個前所未有的新高度。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物