在教授Java時,我經常發現學生嘗試在方法中聲明其它的方法。不過,與Pascal語言不同--Pascal允許嵌套聲明過程procedures(與方法類似),而Java是不允許這樣做的。對于以下的代碼,嘗試在outerMethod()中聲明innerMethod()方法,編譯時將會通不過。
void outerMethod () { void innerMethod () { } } |
不過,由Java語言規范1.1開始,Java就支持類嵌套;Java編譯器允許一個類出現在另一個類中。以下的代碼段就展示了如何在outerClass類中嵌套innerClass類。
class outerClass { class innerClass { } } |
為什么Java支持類嵌套,還有Java支持哪些類嵌套?以下的部分將會回答這些問題,當你讀完這篇文章,你就會學習到類嵌套的相關知識,并且運用它來寫出強大的Java程序。首先我們先來看一下為什么Java支持類嵌套。
注意:在發布JDK1.1后,Sun發布了內部類規范的文檔。該文檔談到嵌套頂級類和內部類的運用。在讀完這篇文章后,我強烈建議你瀏覽這些文檔。
<為什么Java支持類嵌套?
Java并非一定要支持類嵌套。實際上,如果你看過內部類規范文檔,你將會發現類嵌套的應用在哪里。不過,Java支持類嵌套至少有兩個好處:
.令源代碼更加清晰
.減少命名的沖突
通過類嵌套可以令源代碼變得更為清楚,因為你聲明了一個包含有一些對象的類,它必須可以管理和允許類中的方法來直接訪問對象的字段,以及調用對象的方法--即使是外部類中的私有字段和方法。要理解這個好處,可用以下的一個例子說明,在這個程序中要循環遍歷一個Employee對象中的Job對象:
Listing 1. JobIterator1.java
// JobIterator1.java class Job Job (String jobTitle) public String toString () class Employee Employee (String name, Job [] jobs) String getName () boolean hasMoreJobs () Job nextJob () class JobIterator1 Employee e = new Employee ("John Doe", jobs); System.out.println (e.getName () + " works the following jobs:"); while (e.hasMoreJobs ()) |
運行后, JobIterator1產生如下的輸出:
John Doe works the following jobs:
Janitor
Delivery Person
JobIterator1包含有Job, Employee和JobIterator1類。Job封裝了一個job的title,而Employee封裝了一個員工的名字以及該員工所做工作的一個數組。JobIterator1包含有一個main()的方法用來創建Job和Employee對象,并且打印出該員工的名字和工作。
<Employee類中包含有方法hasMoreJobs()和nextJob()。這兩個方法構成了一個循環。當一個Employee對象初始化時,在私有jobs數組中的一個內部索引被設置為0。如果該索引的值比jobs數組的長度少,hasMoreJobs()方法返回一個布爾值true。nextJob()使用該索引值由數組中返回一個Job對象--并且增加該索引的值,以便下一次調用nextJob()時返回下一個job對象的一個引用。
JobIterator1類在設計上有一些問題。首先,在循環結束后你不能重新啟動一個循環。當然,你可以很容易地解決這個問題,只要Employee類加入一個reset()方法就可以了,該方法將jobIndex設置為0。第二個問題更加嚴重,你不能為一個Employee創建多個循環。這是由于hasMoreJobs()和nextJob()方法已經寫死在Employee中了。要解決這兩個問題,開發者可以聲明一個循環類,它的對象可以循環jobs數組。在循環結束后,程序可以通過創建一個新的循環對象來啟動一個新的循環。同樣,通過創建多個循環對象,一個程序可以在同一個Employee對象的jobs數組上執行多個循環。列表2的程序為我們展示了一個名字為JobIterator的循環類:
Listing 2. JobIterator2.java
class Job Job (String jobTitle) public String toString () class Employee Employee (String name, Job [] jobs) String getName () JobIterator getJobIterator () class JobIterator JobIterator (Job [] jobs) boolean hasMoreJobs () Job nextJob () class JobIterator2 Employee e = new Employee ("John Doe", jobs); System.out.println (e.getName () + " works the following jobs:"); JobIterator ji = e.getJobIterator (); while (ji.hasMoreJobs ()) |
JobIterator2的輸出和JobIterator1一樣,但有所不同的是,JobIterator2將循環的代碼由Employee轉移到JobIterator。還有,Employee聲明了一個getJobIterator()的方法,可返回一個新的JobIterator對象引用。要注意到JobIterator和 Employee是緊耦合的類:JobIterator的構造器需要一個Employee私有jobs數組的引用。我們要記下這個耦合關系,因為它為理解類嵌套內部的工作提供了一個線索。
<雖然JobIterator2很方便地解決了JobIterator1的問題,但這個新的程序又引入了一個新問題:由于新加入了一個和Employee類處在同一級的JobIterator類,這樣將來就不能在同一級的源文件中加入一個普遍的JobIterator接口。因為在源文件的同一級中,你不同擁有同樣名字的兩個類/接口。雖然在我們的例子中這不是一個嚴重的問題,但是在重要的程序中,有些情況下必須在同一個源文件中使用相同名字的類/接口。要令那些名字共存,你必須認識到一些類完全依賴其它的類。你應該在其依賴的類中聲明這些類。列表3展示了如何在一個Employee類中聲明一個JobIterator類--JobIterator依賴Employee類。
Listing 3. JobIterator3.java
class Job Job (String jobTitle) public String toString () class Employee Employee (String name, Job [] jobs) String getName () JobIterator getJobIterator () class JobIterator public boolean hasMoreJobs () public Object nextJob () class JobIterator3 Employee e = new Employee ("John Doe", jobs); System.out.println (e.getName () + " works the following jobs:"); Employee.JobIterator eji = e.getJobIterator (); while (eji.hasMoreJobs ()) |
JobIterator3的輸出和JobIterator1、JobIterator2的一樣,它使用了類嵌套:Employee類包含有JobIterator類的聲明。這樣做的結果是,JobIterator不再需要一個構造器,因為JobIterator可直接訪問Employee的私有字段job。還有,在JobIterator3的main()方法中,如果需要訪問JobIterator,你都要加入一個Employee前綴。由于JobIterator3的JobIterator類不再需要一個構造器和自己的jobs字段,因此源代碼要比JobIterator2的清楚一些。
除了令源代碼更加清楚之外,類嵌套還有第二個好處。這就是名字的沖突減少了。在列表3的程序中,頂級的類是Job, Employee和JobIterator3。還有一個Employee.JobIterator類。如果我們在與Employee的同一級中插入一個JobIterator接口,我們就會有Job, Employee, JobIterator3和Employee.JobIterator類,以及一個JobIterator接口。由于Employee.JobIterator和JobIterator表示兩個不同的名字,因此沒有名字沖突。
注意:
如果你編譯JobIterator3并且查看得到的class文件,你將會發現有一個名字為Employee$JobIterator.class的文件。該文件包含有嵌套在Employee中的JobIterator類的字節碼。要注意其中的“$”字符。對于嵌套類,Java的編譯器都會產生一個類似的class名,它以外面的類名和“$”開頭,接著是嵌套類的名字。為什么使用$符號呢?這是因為對于操作系統平臺來說,這是一個合法的字符。而“.”通常是用來隔離文件的擴展名字的,通常都不可在文件名中使用。
Java支持哪些嵌套類?
Java將嵌套類分成兩個主要的分類:嵌套頂級類(nested top-level classes)和內部類(inner classes)。Java還進一步將內部類分為實例內部類(instance inner class) ,本地內部類(local inner class)和匿名內部類(and anonymous inner class)。要掌握嵌套類,你需要懂得每個分類。以下我們就來討論以下這些分類,首先我們來看一下嵌套頂級類。
嵌套頂級類
當你在其它類的外面聲明一個類時,Java就認為該類是一個頂級類。如果你在一個頂級類中聲明一個類,并且在該嵌套類的聲明前加上static的修飾符,你就得到了一個嵌套頂級類。以下的代碼段聲明了一個頂級類和一個嵌套頂級類:
class TopLevelClass { static class NestedTopLevelClass { } } |
就象靜態的字段和靜態的方法一樣(也可稱為類字段和類方法),一個嵌套的頂級類也是與對象無關的。考慮以下的代碼段:
class TopLevelClass static class NestedTopLevelClass { |
在上面NestedTopLevelClass類的類初始化和對象初始化代碼塊中,你可以訪問TopLevelClass的staticField變量。不過你在這兩個代碼塊中都不能訪問instanceField變量。因為NestedTopLevelClass和任何的TopLevelClass對象都是無關的,所以NestedTopLevelClass 不能訪問TopLevelClass的instanceField變量。
警告:一個嵌套頂級類不能訪問任何外部類的實例成員(包括字段和方法)。
雖然NestedTopLevelClass不能訪問TopLevelClass的實例字段,但是static的修飾符并不會妨礙NestedTopLevelClass聲明自己的實例字段,以及創建NestedTopLevelClass對象。列表4的代碼就是一個很好的例子:
Listing 4. NestedTopLevelClassDemo.java
// NestedTopLevelClassDemo.java class TopLevelClass NestedTopLevelClass (int i) class NestedTopLevelClassDemo |
在運行后,NestedTopLevelClassDemo產生以下的輸出:
5
NestedTopLevelClassDemo的main()方法創建了一個NestedTopLevelClass變量--ntlc。聲明該變量的語法和列表3的一樣(Employee.JobIterator eji = e.getJobIterator ())。 通常,當你需要一個嵌套類的變量時,要在該嵌套類名的前面加入所有外部類的名字,并且用“.”將它們分開。用new關鍵字聲明該嵌套類的時候也要這樣做。
這時,你可能想知道你是否可以在一個嵌套頂級類中聲明一個頂級嵌套類。還有,如果兩個不同的外部類聲明了同樣的字段變量名,但該變量的類型或者初始化值是不同的?要解答這些問題,可看列表5:
Listing 5. NestingAndShadowingDemo.java
// NestingAndShadowingDemo.java class TopLevelClass static class NestedTopLevelClass static class NestedNestedTopLevelClass class NestingAndShadowingDemo |
在運行時,NestingAndShadowingDemo產生以下的輸出:
a = 2
b = 3
NestingAndShadowingDemo可以編譯運行證明你可以在頂級嵌套類中嵌套頂級嵌套類。結果也顯示了NestedTopLevelClass的一個字段屏蔽了TopLevelClass的字段。結果是打印出NestedTopLevelClass的字段內容。
使用嵌套頂級類,你不可以訪問外部類的實例字段或者調用該類的實例方法。要訪問實例成員,Java支持內部類。內部類和嵌套頂級類相似,不同的地方是你不會在內部內的聲明前加上static關鍵字。我們以下就討論一下內部類,先從實例內部類開始。
提示:你可以在頂級嵌套類前加上private,protected或者public關鍵字,以指示該類在外部類以外的訪問級別。
<實例內部類(Instance inner classes)
假設你在聲明嵌套類時,沒有在前面加上static關鍵字。這樣你得到的不是一個嵌套頂級類,你得到的是一個實例內部類。嵌套頂級類只可以訪問外部內的static成員,而實例內部類可以同時訪問static和實例成員。列表3的代碼中有實例內部內的例子。在Employee類中,你可以看到實例內部類JobIterator。仔細觀察這兩個類,你可以看到JobIterator可以訪問Employee的private jobs實例字段。
提示:你可以在實例內部類前加上private,protected或者public關鍵字,以指示該類在外部類以外的訪問級別。
本地內部類(Local inner classes)
除了以上的類嵌套外,Java還允許你在任意的代碼段中放入一個類,該類的代碼放在一對大括號中({ })。這意味著類可以出現在一個方法中,甚至是在if語句的一對括號中。這樣的類就稱為本地內部類。
相對于實例內部類來說,本地內部類與有一個好處。它除了可以訪問外部類的實例和類字段外(稱為實例和類方法),還可以訪問本地變量和方法的參數。列表6就是一個本地內部類:
Listing 6. LocalInnerClassDemo.java
import java.util.*; class ComputerLanguage ComputerLanguage (String name) public String toString () class LocalInnerClassDemo Enumeration e = enumerator ((Object []) cl); while (e.hasMoreElements ()) static Enumeration enumerator (final Object [] array) public boolean hasMoreElements () public Object nextElement () return new LocalInnerClass (); |
在運行后, LocalInnerClassDemo產生以下的輸出:
Ada
Algol
APL
Assembly - IBM 360
Assembly - Intel
Assembly - Mostek
Assembly - Motorola
Assembly - VAX
Assembly - Zilog
BASIC
C
C++
Cobol
Forth
Fortran
Java
LISP
Logo
Modula 2
Pascal
Perl
Prolog
Snobol
LocalInnerClassDemo展示了在LocalInnerClassDemo的enumerator()類方法中聲明本地內部類LocalInnerClass。要注意到enumerator()返回一個對象的引用,該對象的類實現了Enumeration接口(在java.util包中)。由于LocalInnerClass也實現了Enumeration,所以enumerator() 合法地返回一個到LocalInnerClass對象的引用。
enumerator()的參數列表中是一個單維數組object[],還有一個關鍵字final。final關鍵字的作用是什么?如果一個本地內部類企圖訪問一個本地變量或者是該類所處方法的一個參數,這些本地變量/參數必須是final的。兩個不同的對象不能共享訪問同樣的非final變量或者參數,否則在設計上就會帶來復雜的同步問題。使用final的本地/參數變量就沒有這些困難。
注意:如果本地變量或者參數中有原始類型的變量,而不是引用的類型,編譯器就用一個常數代替該變量。
<在enumerator() 返回后,LocalInnerClassDemo可以訪問該數組嗎?對于一個參數來說(final或者其它的),會否在一個方法退出后消失呢?是的,該參數消失了;不過,如果你查看一下相應class文件的字節代碼,你將會發現編譯器產生了一些代碼。首先,編譯器在LocalInnerClass中創建了一個LocalInnerClass(Object val$array)構造器。第二,編譯器創建了一個隱含的對象val$array;該字段在LocalInnerClass中。該字段被稱為是一個synthetic字段 ,因為它是由編譯器創建的。最后,該編譯器返回new LocalInnerClass ();在enumerator()中,返回new LocalInnerClass (array);由于LocalInnerClass擁有指向同一個Object []數組(也就是傳送到enumerator()的數組)的引用拷貝。所以在enumerator()的數組參數消失后,LocalInnerClass的方法仍然可以引用該數組。
提示:要知道更多的編譯器是如何管理頂級類中本地內部類,可以學習以下內部類規范的文檔,也可以通過javap程序來反解class文件。
匿名內部類(Anonymous inner classes)
如果一個類很短,你可以聲明一個沒有名字的本地內部類。因為它的名字對該類沒有用處,而且,當同一個類中包含有幾個本地內部類時,如果不選擇一個名字,可以減少名字沖突的機會。沒有名字的本地內部類就是匿名類部類。
由于匿名內部類沒有一個名字,因此你聲明的時候就要創建它,如列表7所示:
Listing 7. AnonymousInnerClassDemo1.java
// AnonymousInnerClassDemo1.java abstract class Farmer Farmer (String name) abstract void occupation (); class BeefFarmer extends Farmer void occupation () class AnonymousInnerClassDemo1 new Farmer ("Jane Doe") |
在運行時,AnonymousInnerClassDemo1產生以下的輸出:
Farmer John Doe raises beef cattle
Farmer Jane Doe milks cows
AnonymousInnerClassDemo1聲明了一個抽象的Farmer類,里面封裝了一個farmer的名字和職業---通過一個抽象的occupation()方法。BeefFarmer類擴展Farmer,并且覆蓋了occupation()來標識beef farmer的工作。該類出現在AnonymousInnerClassDemo1的main()方法中,它創建了一個BeefFarmer對象并且調用它的occupation()方法來打印出該beef farmer的工作。這里并沒有什么奇怪的地方!不過當你繼續查看main()方法時,你會看到一些奇怪的東西:new Farmer ("Jane Doe") { ... }。它看來要由抽象的Farmer類中創建一個對象,這看來是一件不可能的事。不過在("Jane Doe") 下面的大括號又是什么意思呢?看來這不是合法的Java代碼?實際上,這些代碼是合法的,它的含義是:讓JVM由Farmer匿名子類中創建一個對象。該子類覆蓋了Farmer的occupation()方法,并且調用Farmer (String name)的構造器來初始化Farmer的protected字段name。在創建該對象后,調用它的occupation()方法并且丟棄該對象的引用,以便令該對象符合垃圾收集的條件。
AnonymousInnerClassDemo1使用超類的構造器來做初始化,這樣就帶來一個問題:我可以在一個匿名類中聲明自己的構造器嗎?答案是不可以。由于一個構造器需要一個類名,而匿名類是沒有名字的,所以編譯器不能選擇一個名字。
警告:在一個匿名類中聲明構造器是非法的,這是由于構造器的名字要和所處的類名一樣,而匿名內部類是沒有名字的。
<雖然由一個匿名內部類創建一個對象時,你不可以建立構造器,但你可以使用一個對象初始塊來來執行自定義的初始化。例如,如果你想定制AnonymousInnerClassDemo匿名子類Farmer中的Farmer Jane Doe milks cows信息。你想將要擠的奶牛數目通過命令行傳遞過去,并且令該數值出現在信息中。由于一個對象初始塊是在對象創建時執行的,你只要在對象初始化中執行相應的命令行參數初始化就可以了,如列表8所示:
Listing 8. AnonymousInnerClassDemo2.java
// AnonymousInnerClassDemo2.java abstract class Farmer Farmer (String name) abstract void occupation (); class BeefFarmer extends Farmer void occupation () class AnonymousInnerClassDemo2 new Farmer ("Jane Doe") { void occupation () |
假設你在命令行中輸入java AnonymousInnerClassDemo2 10,你就會得到以下的輸出:
Farmer John Doe raises beef cattle
Farmer Jane Doe milks 10 cows
AnonymousInnerClassDemo2仍然通過調用構造器Farmer (String name)來初始化所得對象的Farmer層。不過,所得對象的匿名層也有一個機會初始化,這就是通過對象初始代碼塊。
注意:雖然一個匿名內部類沒有名字,編譯器仍然需要為該class文件產生一個名字。編譯器選擇的是一個整數,該數字放在外部類和$符號的后面,作為匿名內部類的名字。例如,在AnonymousInnerClassDemo2中,編譯器為該匿名內部類產生的class文件名是AnonymousInnerClassDemo2$1.class。
在結束這部分前,這里提及一下匿名內部類的實際應用:開發者經常使用匿名內部類來簡化事件處理--即重要事件的通告,例如移動鼠標或者按一個按鈕--在程序中會產生和顯示一個圖形用戶界面(GUIs)。使用匿名內部類作事件處理是很方便的,因為這些處理通常都不需要一個類名。列表9就是這樣一個例子,這里的匿名內部類簡化了windows-closing的事件處理。
Listing 9. AnonymousInnerClassDemo3.java
// AnonymousInnerClassDemo3.java import java.awt.*; class AnonymousInnerClassDemo3 Frame f = new Frame ("Anonymous Inner Class Demo #3"); // Add a window listener that will generate a window closing event f.addWindowListener (new WindowAdapter () // Establish the frame window's size as 300 horizontal pixels by f.setSize (300, 100); // Display the frame window and get the underlying event handling f.setVisible (true); |
在運行時,AnonymousInnerClassDemo3顯示了一個矩形的窗口,即框架窗口,它是所得GUI的主要窗口。在框架窗口出現后,用戶可以移除該窗口并且停止AnonymousInnerClassDemo3的執行。這通常在用戶點擊窗口右上角的X按鈕時發生。
當用戶按X時,底層的Java windowing工具包創建了一個事件對象,并且調用一個特別的方法--windowClosing (WindowEvent e),e包含有該事件對象的一個引用--該方法放在一個被稱為frame窗口的window監聽者(window listener)對象中。該監聽者對象通過調用Frame的addWindowListener (WindowListener wl)方法注冊到底層的工具包中(因此工具包可以知道方法的位置)。
WindowListener 接口聲明了幾個方法,每個方法都用作一個可能的window事件。開發者不必實現全部這些方法,因為Java的windowing工具包的設計者建立了一個WindowAdapter類,它實現了全部的WindowListener方法。不過都是空的方法體,開發者可以聲明一個匿名內部類來擴展WindowAdapter,并且實現其中的一個或者多個方法。如上所示,我選擇覆蓋windowClosing (WindowEvent e)方法,并且在其中調用System.exit (0);這樣可以通過windowing工具包調用windowClosing (WindowEvent e),就可以終止一個程序(同時關閉窗口)。
注意:在為你的GUI開發事件處理時,你將會經常用到匿名內部類。在以后的文章中,我將會提供許多由匿名內部類來進行事件處理的例子。
總結
這篇文章介紹了類嵌套對于Java語言是很有用的。類嵌套不但令源代碼變得清晰--因為你可以在類要管理的對象附近聲明類--它同樣可以減少一個源文件中,在同一級中聲明類的名字的沖突機會。
有4種不同的嵌套類:nested top-level classes(嵌套頂級類), instance inner classes(實例內部類), local inner classes(本地內部類)和anonymous inner classes(匿名內部類)。嵌套頂級類只可以訪問外部類的類級字段和調用類級的方法,而實例內部類可以訪問外部類的類級和實例字段,并且調用一個外部類的類級或者實例方法。嵌套頂級類和實例內部類只可以出現在其它類的定義中,Java還提供了本地內部類,可以出現在任意的代碼塊中--包括一個方法塊或者一個if語句的代碼塊。由于一些本地內部類很短,因此可以不使用名字,Java提供了匿名內部類。本地和匿名內部類可以訪問外部代碼塊中本地的變量和參數,但要注意的是這些本地變量和參數必須是標識為final的。