目前見到的類履恩、接口和枚舉類型都定義為頂層類型。也就是說呢蔫,都是包的直接成員切心,獨立于其他類型。不過片吊,類型還可以嵌套在其他類型中定義绽昏。這種類型是嵌套類型(nested type),一般稱為“內(nèi)部類”俏脊,是 Java 語言的一個強大功能全谤。
嵌套類型有兩個獨立的目的,但都和封裝有關(guān)爷贫。
如果某個類型需要特別深入地訪問另一個類型的內(nèi)部實現(xiàn)认然,可以嵌套定義這個類型。作為成員類型的嵌套類型漫萄,其訪問方式與訪問成員變量和方法的方式一樣卷员,而且能打破封裝的規(guī)則。
某個類型可能只在特定的情況下需要使用腾务,而且只在非常小的代碼區(qū)域使用毕骡。這個類型應(yīng)該密封在一個小范圍內(nèi)岩瘦,因為它其實是實現(xiàn)細(xì)節(jié)的一部分未巫,應(yīng)該封裝在一個系統(tǒng)的其他部分無法接觸到的地方担钮。
嵌套類型也可以理解為通過某種方式和其他類型綁定在一起的類型橱赠,不作為完全獨立的實體真實存在。類型能通過四種不同的方式嵌套在其他類型中箫津。
靜態(tài)成員類型
靜態(tài)成員類型是定義為其他類型靜態(tài)成員的類型狭姨。嵌套的接口、枚舉和注解始終都是靜態(tài)成員類型(就算不使用 static 關(guān)鍵字也是)苏遥。
非靜態(tài)成員類
“非靜態(tài)成員類型”就是沒使用 static 聲明的成員類型饼拍。只有類才能作為非靜態(tài)成員類型。
局部類
局部類是在 Java 代碼塊中定義的類田炭,只在這個塊中可見师抄。接口、枚舉和注解不能定義為局部類型教硫。
匿名類
匿名類是一種局部類叨吮,但對 Java 語言來說沒有有意義的名稱辆布。接口、枚舉和注解不能定義為匿名類型茶鉴。
“嵌套類型”這個術(shù)語雖然正確且準(zhǔn)確锋玲,但開發(fā)者并沒有普遍使用,大多數(shù) Java 程序員使用的是一個意義模糊的術(shù)語——“內(nèi)部類”涵叮。根據(jù)語境的不同惭蹂,這個術(shù)語可以指代非靜態(tài)成員類、局部類或匿名類割粮,但不能指代靜態(tài)成員類型盾碗,因此使用“內(nèi)部類”這個術(shù)語時無法區(qū)分指代的是哪種嵌套類型。
雖然表示各種嵌套類型的術(shù)語并不總是那么明確舀瓢,但幸運的是廷雅,從語境中一般都能確定應(yīng)該使用哪種句法。
下面詳細(xì)介紹這四種嵌套類型氢伟。每種類型都用單獨的一節(jié)介紹其特性榜轿,使用時的限制,以及專用的 Java 句法朵锣。介紹完這四種嵌套類型之后谬盐,還有一節(jié)說明嵌套類型的運作方式。
靜態(tài)成員類型
靜態(tài)成員類型和普通的頂層類型很像诚些,但為了方便飞傀,把它嵌套在另一個類型的命名空間中。靜態(tài)成員類型有如下的基本特性诬烹。
靜態(tài)成員類型類似于類的其他靜態(tài)成員:靜態(tài)字段和靜態(tài)方法;
靜態(tài)成員類型和所在類的任何實例都不關(guān)聯(lián)(即沒有this對象);
靜態(tài)成員類型只能訪問所在類的靜態(tài)成員;
靜態(tài)成員類型能訪問所在類型中的所有靜態(tài)成員(包括其他靜態(tài)成員類型);
不管使不使用 static 關(guān)鍵字砸烦,嵌套的接口、枚舉和注解都隱式聲明為靜態(tài)類型;
接口或注解中的嵌套類型也都隱式聲明為靜態(tài)類型;
靜態(tài)成員類型可以在頂層類型中定義绞吁,也可以嵌入任何深度的其他靜態(tài)成員類型中;
靜態(tài)成員類型不能在其他嵌套類型中定義幢痘。
下面通過一個簡單的例子介紹靜態(tài)成員類型的句法。示例定義了一個輔助接口家破,是所在類的靜態(tài)成員颜说。這個示例還展示了如何在定義這個接口的類內(nèi)部以及外部的類中使用這個接口。注意汰聋,在外部類中要使用這個接口在層次結(jié)構(gòu)中的名稱门粪。
靜態(tài)成員類型能訪問所在類型中的所有靜態(tài)成員,包括私有成員烹困。反過來也成立:所在類型的方法能訪問靜態(tài)成員類型中的所有成員玄妈,包括私有成員。靜態(tài)成員類型甚至能訪問任何其他靜態(tài)成員類型中的所有成員,包括這些類型的私有成員拟蜻。靜態(tài)成員類型使用其他靜態(tài)成員時绎签,無需使用所在類型的名稱限定成員的名稱。
靜態(tài)成員類型不能和任何一個外層類同名瞭郑。而且辜御,靜態(tài)成員類型只能在頂層類型和其他靜態(tài)成員類型中定義,也就是說屈张,靜態(tài)成員類型不能在任何成員類、局部類或匿名類中定義袱巨。
頂層類型可以聲明為 public 或?qū)Π接?即聲明時沒使用 public 關(guān)鍵字)阁谆。但是把頂層類型聲明為 private 或 protected 都沒什么意義——protected 和對包私有其實一樣,而任何其他類型都不能訪問聲明為 private 的頂層類愉老。
然而场绿,靜態(tài)成員類型是一種成員,因此所在類型中的成員能使用的訪問控制修飾符嫉入,靜態(tài)成員類型都能使用焰盗。這些修飾符對靜態(tài)成員類型來說,作用與用在類型的其他成員上一樣咒林。前面說過熬拒,接口(和注解)的所有成員都隱式聲明為 public,所以嵌套在接口或注解類型中的靜態(tài)成員類型不能聲明為 protected 或 private垫竞。
例如澎粟,在示例中,Linkable 接口聲明為 public欢瞪,因此任何想存儲 LinkedStack 對象的類都可以實現(xiàn)這個接口活烙。
在所在類外部,靜態(tài)成員類型的名稱由外層類型的名稱和內(nèi)層類型的名稱組成(例如遣鼓,LinkedStack.Link able)啸盏。
大多數(shù)情況下,這種句法有助于提醒內(nèi)層類和所在的類型有內(nèi)在聯(lián)系骑祟。不過回懦,Java 語言允許使用 import 指令直接或間接導(dǎo)入靜態(tài)成員類型:
還可以使用 import static 指令導(dǎo)入靜態(tài)成員類型。
但是曾我,導(dǎo)入嵌套類型模糊了這個類型和外層類型之間的關(guān)系粉怕,而這種關(guān)系往往很重要,因此很少這么做抒巢。
非靜態(tài)成員類
非靜態(tài)成員類聲明為外層類或枚舉類型的成員贫贝,而且不使用 static 關(guān)鍵字:
如果把靜態(tài)成員類型比作類字段或類方法,那么非靜態(tài)成員類可以比作實例字段或?qū)嵗?/p>
方法;
只有類才能作為非靜態(tài)成員類型;
一個非靜態(tài)成員類的實例始終關(guān)聯(lián)一個外層類型的實例;
非靜態(tài)成員類的代碼能訪問外層類型的所有字段和方法(靜態(tài)和非靜態(tài)的都能訪問);
為了讓非靜態(tài)成員類訪問外層實例,Java 提供了幾個專用的句法稚晚。
示例展示了如何定義和使用成員類崇堵。這個示例以前面定義的 LinkedStack 類為基礎(chǔ),增加了 iterator() 方法客燕。這個方法返回一個實現(xiàn) java.util.Iterator 接口的實例鸳劳,枚舉棧中的元素。實現(xiàn)這個接口的類定義為一個成員類也搓。
注意赏廓,LinkedIterator 類嵌套在 LinkedStack 類中。因為 LinkedIterator 是輔助類傍妒,只在LinkedStack 類中使用幔摸,所以在離外層類很近的地方定義,能清晰地表達(dá)設(shè)計意圖——介紹嵌套類型時說過這一點颤练。
1. 成員類的特性
與實例字段和實例方法一樣既忆,非靜態(tài)成員類的每個實例都和外層類的一個實例關(guān)聯(lián)。也就是說嗦玖,成員類的代碼能訪問外層類實例的所有實例字段和實例方法(以及靜態(tài)成員)患雇,包括聲明為 private 的實例成員。
這個重要的特性在上面示例中已經(jīng)體現(xiàn)出來了宇挫。下面再次列出構(gòu)造方法 LinkedStack.
LinkedIterator():
public LinkedIterator() { current = head; }
這一行代碼把內(nèi)層類的 current 字段設(shè)為外層類中 head 字段的值苛吱。即便 head 是外層類的私有字段,也不影響這行代碼的正常運行捞稿。
非靜態(tài)成員類和類的任何成員一樣又谋,可以使用一個標(biāo)準(zhǔn)的訪問控制修飾符。在上面示例中娱局,LinkedIterator 類聲明為 protected彰亥,所以使用 LinkedStack 類的代碼(不同包)不能訪問LinkedIterator 類,但是 LinkedStack 的子類可以訪問衰齐。
2. 成員類的限制
成員類有兩個重要的限制任斋。
? 非靜態(tài)成員類不能和任何外層類或包同名。這是一個重要的規(guī)則耻涛,但不適用于字段和方法废酷。
? 非靜態(tài)成員類不能包含任何靜態(tài)字段、方法或類型抹缕,不過可以包含同時使用 static 和final 聲明的常量字段澈蟆。
靜態(tài)成員是頂層結(jié)構(gòu),不和任何特定的對象關(guān)聯(lián)卓研,而非靜態(tài)成員類和外層類的實例關(guān)聯(lián)趴俘。在成員類中定義頂層靜態(tài)成員會讓人困惑睹簇,因此禁止這么做。
3. 成員類的句法
成員類最重要的特性是可以訪問外層對象的實例字段和方法寥闪。從示例 4-2 中的構(gòu)造方法LinkedStack.LinkedIterator() 可以看出這一點:
public LinkedIterator() { current = head; }
在這個示例中太惠,head 字段是外層 LinkedStack 類的字段,我們把這個字段的值賦值給LinkedIterator 類的 current 字段(current 是非靜態(tài)成員類的一個成員)疲憋。
如果想使用this 顯式引用凿渊,就要使用一種特殊的句法,顯式引用 this 對象表示的外層實例缚柳。例如埃脏,如果想在這個構(gòu)造方法中顯式引用,可以使用下述句法:
public LinkedIterator() { this.current = LinkedStack.this.head; }
這種句法的一般形式是 classname.this秋忙,其中 classname 是外層類的名稱剂癌。注意,成員類中可以包含成員類翰绊,嵌套的層級不限。然而旁壮,因為成員類不能和任何外層類同名监嗜,所以,在 this 前面使用外層類的名稱是引用任何外層實例最好的通用方式抡谐。
僅當(dāng)引用的外層類成員被成員類的同名成員遮蓋時才必須使用這種特殊的句法裁奇。
局部類
局部類在一個 Java 代碼塊中聲明,不是類的成員麦撵。只有類才能局部定義刽肠,接口、枚舉類型和注解類型都必須是頂層類型或靜態(tài)成員類型免胃。局部類往往在方法中定義音五,但也可以在類的靜態(tài)初始化程序或?qū)嵗跏蓟绦蛑卸x。
因為所有 Java 代碼塊都在類中羔沙,所以局部類都嵌套在外層類中躺涝。因此,局部類和成員類有很多共同的特性扼雏。局部類往往更適合看成完全不同的嵌套類型坚嗜。
局部類的典型特征是局部存在于一個代碼塊中。和局部變量一樣诗充,局部類只在定義它的塊中有效苍蔬。下面示例修改 LinkedStack 類的 iterator() 方法,把 LinkedIterator 類從成員類改成局部類蝴蜓。
這樣修改之后碟绑,我們把 LinkedIterator 類的定義移到離使用它更近的位置,希望更進(jìn)一步提升代碼的清晰度。簡單起見蜈敢,下面示例只列出了 iterator() 方法辜荠,沒有寫出包含它的整個 LinkedStack 類。
1. 局部類的特性
局部類有如下兩個有趣的特性:
? 和成員類一樣抓狭,局部類和外層實例關(guān)聯(lián)伯病,而且能訪問外層類的任何成員,包括私有成員;
? 除了能訪問外層類定義的字段之外否过,局部類還能訪問局部方法的作用域中聲明為 final的任何局部變量午笛、方法參數(shù)和異常參數(shù)。
2. 局部類的限制
局部類有如下限制苗桂。
局部類的名稱只存在于定義它的塊中药磺,在塊的外部不能使用。(但是要注意煤伟,在類的作用域中創(chuàng)建的局部類實例癌佩,在這個作用域之外仍能使用。稍后本節(jié)會詳細(xì)說明這種情況便锨。)
局部類不能聲明為 public围辙、protected、private 或 static放案。
與成員類的原因一樣姚建,局部類不能包含靜態(tài)字段、方法或類吱殉。唯一的例外是同時使用static 和 final 聲明的常量掸冤。
接口、枚舉類型和注解類型不能局部定義友雳。
局部類和成員類一樣稿湿,不能與任何外層類同名。
前面說過沥阱,局部類能使用同一個作用域中的局部變量缎罢、方法參數(shù)和異常參數(shù),但這些變量或參數(shù)必須聲明為 final考杉。這是因為策精,局部類實例的生命周期可能比定義它的方法的執(zhí)行時間長很多。
局部類用到的每個局部變量都有一個私有內(nèi)部副本(這些副本由 javac 自動生成)崇棠。只有把局部變量聲明為 final 才能保證局部變量和私有副本始終保持一致咽袜。
3. 局部類的作用域
介紹非靜態(tài)成員類時,我們知道枕稀,成員類能訪問繼承自超類的任何成員以及外層類定義的任何成員询刹。這對局部類來說也成立谜嫉,但局部類還能訪問聲明為 final 的局部變量和參數(shù)。下面示例展示了局部變量能訪問的不同字段和變量種類:
詞法作用域和局部變量
局部變量在一個代碼塊中定義凹联,這個代碼塊是這個變量的作用域沐兰,在這個作用域之外無法訪問這個局部變量,局部變量也不復(fù)存在蔽挠∽〈常花括號劃定塊的邊界,花括號中的任何代碼都能使用這個塊中定義的局部變量澳淑。
這種作用域是詞法作用域比原,定義變量能在哪一塊源碼種使用。程序員一般可以把這種作用域理解為暫時存在的事物杠巡,而不能認(rèn)為局部變量的存在時間是從 JVM 開始執(zhí)行代碼塊開始量窘,到退出代碼塊為止。像這樣理解局部變量和它的作用域一般是合理的氢拥。
但是蚌铜,局部類的出現(xiàn)把這個局面攪亂了。注意嫩海,局部類的實例可能在 JVM 退出定義這個局部類的代碼塊后依然存在厘线,這就是原因。
也就是說出革,如果創(chuàng)建了局部類的一個實例,那么渡讼,JVM 執(zhí)行完定義這個類的代碼塊后骂束,實例不會自動消失。因此成箫,即便這個類在局部定義展箱,但這個類的實例能跳出定義它的地方。
這可能會導(dǎo)致一些效果蹬昌,讓某些初次接觸的開發(fā)者驚訝混驰。這是因為,局部類能使用局部變量皂贩,而且會從不復(fù)存在的詞法作用域中創(chuàng)建變量值的副本栖榨。這一點從下述代碼種可以看出:
為了理解這段代碼,要記住一點明刷,局部類中方法的詞法作用域與解釋器進(jìn)出定義局部類的代碼塊沒有任何聯(lián)系婴栽。
局部類的各個實例用到的每個 final 局部變量,都會自動創(chuàng)建一個私有副本辈末,因此愚争,得到的效果是映皆,創(chuàng)建實例時,這個實例擁有一個所在作用域的私有副本轰枝。
局部類 MyIntHolder 有時也叫閉包(closure)捅彻。用更一般的 Java 術(shù)語來說,閉包是一個對象鞍陨,它保存作用域的狀態(tài)步淹,并讓這個作用域在后面可以繼續(xù)使用。
在某些編程風(fēng)格中閉包是有用的湾戳。不同的編程語言使用不同的方式定義和實現(xiàn)閉包贤旷,Java通過局部類、匿名類和 lambda 表達(dá)式實現(xiàn)閉包砾脑。
匿名類
匿名類是沒有名稱的局部類幼驶,使用 new 運算符在一個簡潔的表達(dá)式中定義和實例化。局部類是 Java 代碼塊中的一個語句韧衣,而匿名類是一個表達(dá)式盅藻,因此可以包含在大型表達(dá)式中,例如方法調(diào)用表達(dá)式畅铭。
為了完整介紹嵌套類氏淑,這里涵蓋了匿名類,但在 Java 8 之后硕噩,大多數(shù)情況下都把匿名類換成了 lambda 表達(dá)式假残。
下面示例在 LinkedStack 類的 iterator() 方法中使用匿名類實現(xiàn) LinkedIterator 類。和上面示例對比一下炉擅,上面示例使用局部類實現(xiàn)了同一個類辉懒。
可以看出,定義匿名類和創(chuàng)建這個類的實例使用 new 關(guān)鍵字谍失,后面跟著某個類的名稱和放在花括號里的類主體眶俩。如果 new 關(guān)鍵字后面是一個類的名稱,那么這個匿名類是指定類的子類快鱼。如果 new 關(guān)鍵字后面是一個接口的名稱颠印,如前面的示例所示,那么這個匿名類實現(xiàn)指定的接口抹竹,并且擴展 Object 類线罕。
匿名類使用的句法無法指定 extends 子句和 implements 子句,也不能為這個類指定名稱窃判。
因為匿名類沒有名稱闻坚,所以不能在類主體中定義構(gòu)造方法。這是匿名類的一個基本限制兢孝。定義匿名類時窿凤,在父類后面的括號中指定的參數(shù)仅偎,會隱式傳給父類的構(gòu)造方法。匿名類一般用于創(chuàng)建構(gòu)造方法不接受任何參數(shù)的簡單類的子類雳殊,所以橘沥,在定義匿名類的句法中,括號經(jīng)常都是空的夯秃。前面示例中的匿名類實現(xiàn)一個接口并擴展 Object 類座咆。因為構(gòu)造方法Object() 不接受參數(shù),所以括號是空的仓洼。
匿名類的限制
匿名類就是一種局部類介陶,所以二者的限制一樣。除了使用 static final 聲明的常量之外色建,匿名類不能定義任何靜態(tài)字段哺呜、方法和類。接口箕戳、枚舉類型和注解類型不能匿名定義某残。而且,和局部類一樣陵吸,匿名類不能聲明為 public玻墅、private、protected 或 static壮虫。
定義匿名類的句法既定義了這個類也實例化了這個類澳厢。如果每次執(zhí)行外層塊時創(chuàng)建的實例不止一個,那么就不能用匿名類代替局部類囚似。
因為匿名類沒有名稱赏酥,所以無法為匿名類定義構(gòu)造方法。如果類需要構(gòu)造方法谆构,必須使用局部類。不過框都,經(jīng)嘲崴兀可以使用實例初始化程序代替構(gòu)造方法。
雖然實例初始化程序不僅限于在匿名類中使用魏保,但就是為了這個目的才把這種功能引入 Java 語言的。匿名類不能定義構(gòu)造方法,所以只有一個默認(rèn)構(gòu)造方法跪帝。使用實例初始化程序可以打破匿名類不能定義構(gòu)造方法這個限制顶别。
嵌套類型的運作方式
前面說明了這四種嵌套類型的特性和行為。對于嵌套類型檩咱,尤其只是為了使用揭措,你要知道的就這么多胯舷。不過,理解嵌套類型的運作方式后能更好地理解嵌套類型绊含。
引入嵌套類型后桑嘶,Java 虛擬機和 Java 類文件的格式并沒有變化。對 Java 解釋器而言躬充,并沒有所謂的嵌套類型逃顶,所有類都是普通的頂層類。
為了讓嵌套類型看起來是在另一個類中定義的充甚,Java 編譯器會在它生成的類中插入隱藏字段以政、方法和構(gòu)造方法參數(shù)。這些隱藏字段和方法經(jīng)常稱為合成物(synthetic)伴找。
你可以使用反匯編程序 javap(第 13 章會介紹)反匯編某些嵌套類型的類文件盈蛮,了解為了支持嵌套類型,編譯器用了什么技巧疆瑰。
為了實現(xiàn)嵌套類型眉反,javac 把每個嵌套類型編譯為單獨的類文件,得到的其實是頂層類穆役。編譯得到的類文件使用特殊的命名約定寸五,這些名稱一般在用戶的代碼中無法創(chuàng)建。
在第一個 LinkedStack 類的示例(示例 4-1)中耿币,定義了一個名為 Linkable 的靜態(tài)成員接口梳杏。編譯這個 LinkedStack 類時,編譯器會生成兩個類文件淹接,第一個是預(yù)期的 LinkedStack.class十性。
不過,第二個類文件名為 LinkedStack$Linkable.class塑悼,其中劲适,$ 由 javac 自動插入。這個類文件中包含的就是靜態(tài)成員接口 Linkable 的實現(xiàn)厢蒜。
因為嵌套類型編譯成普通的頂層類霞势,所以不能直接訪問外層類型中有特定權(quán)限的成員。因此斑鸦,如果靜態(tài)成員類型使用了外層類型的私有成員(或具有其他權(quán)限的成員)愕贡,編譯器會生成合成的訪問方法(具有默認(rèn)的包訪問權(quán)限),然后把訪問私有成員的表達(dá)式轉(zhuǎn)換成調(diào)用合成方法的表達(dá)式巷屿。
這四種嵌套類型的類文件使用如下命名約定固以。
(靜態(tài)或非靜態(tài))成員類型:根據(jù) EnclosingType$Member.class 格式命名成員類型的類文件。
匿名類:因為匿名類沒有名稱嘱巾,所以類文件的名稱由實現(xiàn)細(xì)節(jié)決定憨琳。Oracle/OpenJDK 中的 javac使用數(shù)字表示匿名類的名稱(例如 EnclosingType$1.class)诫钓。
局部類:局部類的類文件綜合使用前兩種方式命名(例如 EnclosingType$1Member.class)。
下面簡單介紹一些實現(xiàn)細(xì)節(jié)栽渴,看一下 javac 如何為每種嵌套類型提供所需的合成訪問能力尖坤。
1. 非靜態(tài)成員類的實現(xiàn)
非靜態(tài)成員類的每個實例都和一個外層類的實例關(guān)聯(lián)。為了實現(xiàn)這種關(guān)聯(lián)闲擦,編譯器為每個成員類定義了一個名為 this$0 的合成字段慢味。這個字段的作用是保存一個外層實例的引用。
編譯器為每個非靜態(tài)成員類的構(gòu)造方法提供了一個額外的參數(shù)墅冷,用于初始化這個字段纯路。每次調(diào)用成員類的構(gòu)造方法時,編譯器都會自動把這個額外參數(shù)的值設(shè)為外層類的引用寞忿。
2. 局部類和匿名類的實現(xiàn)
局部類之所以能訪問外層類的字段和方法驰唬,原因和非靜態(tài)成員類一模一樣:編譯器把一個外層類的隱藏引用傳入局部類的構(gòu)造方法,并且把這個引用存儲在編譯器合成的一個私有字段中腔彰。和非靜態(tài)成員類一樣叫编,局部類也能使用外層類的私有字段和方法,因為編譯器會插入任何所需的訪問器方法霹抛。
局部類和成員類的不同之處在于搓逾,局部類能訪問所在塊中的局部變量。不過這種能力有個重要的限制杯拐,即局部類只能訪問聲明為 final 的局部變量和參數(shù)霞篡。這個限制的原因從實現(xiàn)中可以清楚地看出來。
局部類之所以能使用局部變量端逼,是因為 javac 自動為局部類創(chuàng)建了私有實例字段朗兵,保存局部類用到的各個局部變量的副本。
編譯器還在局部類的構(gòu)造方法中添加了隱藏的參數(shù)顶滩,初始化這些自動創(chuàng)建的私有字段余掖。其實,局部類沒有訪問局部變量礁鲁,真正訪問的是局部變量的私有副本盐欺。如果在局部類外部能修改局部變量,就會導(dǎo)致不一致性救氯。