開篇在我的上一篇博客 深入理解Java中為什么內(nèi)部類可以訪問外部類的成員 中, 通過使用javap工具反編譯內(nèi)部類的字節(jié)碼, 我們知道了為什么內(nèi)部類中可以訪問外部類的成員, 其實(shí)是編譯器在編譯內(nèi)部類的class文件時(shí),偷偷做了一些工作, 使內(nèi)部類持有外部類的引用, 并且通過在構(gòu)造方法上添加參數(shù)注入這個(gè)引用, 在調(diào)用構(gòu)造方法時(shí)默認(rèn)傳入了外部類的引用。 我們之所以感到疑惑, 就是因?yàn)榫幾g器使用的障眼法。當(dāng)我們把字節(jié)碼反編譯出來之后, 編譯器的這些小伎倆就會(huì)清清楚楚的展示在我們面前。 感興趣的朋友可以移步到上一篇博客, 博客鏈接: http://blog.csdn.NET/zhangjg_blog/article/details/20000769
在本文中, 我們要對(duì)定義在方法中的內(nèi)部類進(jìn)行分析。 和上一篇博客一樣, 我們還是使用javap工具對(duì)內(nèi)部類的字節(jié)碼進(jìn)行解剖。 并且和上一篇文章進(jìn)行對(duì)比分析, 探究定義在外部類方法中的內(nèi)部類和定義在外部類中的內(nèi)部類有哪些相同之處和不同之處。 這篇博客的講解以上一篇為基礎(chǔ), 對(duì)這些知識(shí)點(diǎn)不是很熟悉的同學(xué), 強(qiáng)烈建議先讀上一篇博客。 博客的鏈接已經(jīng)在上面給出。
定義在方法中的內(nèi)部類在平時(shí)寫代碼的過程中, 我們經(jīng)常會(huì)寫類似下面的代碼段:
這段代碼在main方法中定義了一個(gè)匿名內(nèi)部類, 并且創(chuàng)建了匿名內(nèi)部類的一個(gè)對(duì)象, 使用這個(gè)對(duì)象調(diào)用了匿名內(nèi)部類中的方法。 所有這些操作都在new Thread(){}.start() 這一句代碼中完成, 這不禁讓人感嘆java的表達(dá)能力還是很強(qiáng)的。 上面的代碼和以下代碼等價(jià):
這里我們不關(guān)心方法中匿名內(nèi)部類和非匿名內(nèi)部類的區(qū)別, 我們只需要知道, 這兩種方式都是定義在方法中的內(nèi)部類, 他們的工作原理是相同的。 在本文中主要根據(jù)非匿名內(nèi)部類講解。
讓我們仔細(xì)觀察上面的代碼都有哪些“奇怪”的行為:
1 在外部類的main方法中有一個(gè)局部變量count, 并且在內(nèi)部類的run方法中訪問了這個(gè)count變量。 也就是說, 方法中定義的內(nèi)部類, 可以訪問方法中的局部變量(方法的參數(shù)也是局部變量); 2 count變量使用final關(guān)鍵字修飾, 如果去掉final, 則編譯失敗。 也就是說被方法中的內(nèi)部類訪問的局部變量必須是final的。
由于我們經(jīng)常這樣做, 這樣寫代碼, 久而久之養(yǎng)成了習(xí)慣, 就成了司空見慣的做法了。 但是如果要問為什么Java支持這樣的做法, 恐怕很少有人能說的出來。 在下面, 我們就會(huì)分析為什么Java支持這種做法, 讓我們不僅知其然, 還要知其所以然。
為什么定義在方法中的內(nèi)部類可以訪問方法中的局部變量?1 當(dāng)被訪問的局部變量是編譯時(shí)可確定的字面常量時(shí)我們首先看這樣一段代碼, 本文的以下部分會(huì)以這樣的代碼進(jìn)行講解。
在外部類的方法outerMethod中定義了成員變量 String localVar, 并且用一個(gè)編譯時(shí)字面量"abc"給他賦值。在 outerMethod方法中定義了內(nèi)部類Inner, 并且在內(nèi)部類的方法innerMethod中訪問了localVar變量。 接下來我們就根據(jù)這個(gè)例子來講解為什么可以這樣做。 首先看編譯后的文件, 和普通的內(nèi)部類一樣, 定義在方法中的內(nèi)部類在編譯之后, 也有自己獨(dú)立的class文件:
和普通內(nèi)部類的區(qū)別是, 普通內(nèi)部類的class文件名為Outer$Inner.class 。 而定義在方法中的內(nèi)部類的class文件名為Outer$<N>Inner.class 。 N代表數(shù)字, 如1, 2, 3 等 。 在外部類第一個(gè)方法中定義的內(nèi)部類, 編號(hào)為1, 同理在外部類第二個(gè)方法中定義的內(nèi)部類編號(hào)為2, 在外部類中第N個(gè)方法中定義的內(nèi)部類編號(hào)為N 。 這些都是題外話, 主要想說的是, 方法中的內(nèi)部類也有自己獨(dú)立的class文件。
我們通過javap反編譯工具, 把 Outer$1Inner.class 反編譯成可讀的形式。 關(guān)于javap工具的使用, 請(qǐng)參考我的上一篇博客。 反編譯的輸出結(jié)果如下:
innerMethod方法中一共就以下有三個(gè)指令: 0: ldc #20 // String abc
2: astore_1 3: return Idc指令的意思是將索引指向的常量池中的項(xiàng)壓入操作數(shù)棧。 這里的索引為20 , 引用的常量池中的項(xiàng)為字符串“abc” 。 這句話就揭示了內(nèi)部類訪問方法局部變量的原理。 讓我們從常量池第20項(xiàng)看起。
常量池中第20項(xiàng)確實(shí)是字符串“abc” 。 但是這個(gè)字符串“abc”明明是定義在外部類Outer中的, 因?yàn)槌霈F(xiàn)在外部類的outerMethod方法中。 為了查看這個(gè)“abc”是否在外部類中, 我們繼續(xù)反編譯外部類Outer.class 。 為了篇幅考慮, 在這里指給出Outer.class反編譯輸出的常量池的一部分。
我們可以看到, “abc”這個(gè)字符串確實(shí)出現(xiàn)在Outer.class常量池的第15項(xiàng)。 這就奇怪了, 明明是定義在外部類的字面量, 為什么會(huì)出現(xiàn)在 內(nèi)部類的常量池中呢? 其實(shí)這正是編譯器在編譯方法中定義的內(nèi)部類時(shí), 所做的額外工作。 下面我們將這個(gè)被內(nèi)部類訪問的局部變量改成整形的。 看看在字節(jié)碼層面上會(huì)有什么變化。 修改后的源碼如下:
內(nèi)部類反編譯后的class文件如下: (由于在這里常量池不是重點(diǎn), 所以省略了常量池信息)
從上面的輸出可以看到, innerMethod方法中的第一句字節(jié)碼為:
這句字節(jié)碼的意義是:將int類型的常量 1 壓入操作數(shù)棧。 這就是在內(nèi)部類中訪問外部類方法中的局部變量int localVar = 1的原理。 由此可見, 當(dāng)內(nèi)部類中訪問的局部變量是int型的字面量時(shí), 編譯器直接將對(duì)該變量的訪問嵌入到內(nèi)部類的字節(jié)碼中, 也就是說, 在運(yùn)行時(shí), 方法中的內(nèi)部類和外部類, 和外部類方法中的局部變量就沒有任何關(guān)系了。 這也是編譯器所做的額外工作。 上面兩種情況有一個(gè)共同點(diǎn), 那就是, 被內(nèi)部類訪問的外部了方法中的局部變量, 都是在編譯時(shí)可以確定的字面常量。 像下面這樣的形式都是編譯時(shí)可確定的字面常量:
他們之所以被稱為字面常量, 是因?yàn)樗麄儽籪inal修飾, 運(yùn)行時(shí)不可改變, 當(dāng)編譯器在編譯源文件時(shí), 可以確定他們的值, 也可以確定他們?cè)谶\(yùn)行時(shí)不會(huì)被修改, 所以可以實(shí)現(xiàn)類似C語(yǔ)言宏替換的功能。也就是說雖然在編寫源代碼時(shí), 在另一個(gè)類中訪問的是當(dāng)前類定義的這個(gè)變量, 但是在編譯成字節(jié)碼時(shí), 卻把這個(gè)變量的值放入了訪問這個(gè)變量的另一個(gè)類的常量池中, 或直接將這個(gè)變量的值嵌入另一個(gè)類的字節(jié)碼指令中。 運(yùn)行時(shí)這兩個(gè)類各不相干, 各自訪問各自的常量池, 各自執(zhí)行各自的字節(jié)碼指令。在編譯方法中定義的內(nèi)部類時(shí), 編譯器的行為就是這樣的。 那么當(dāng)方法中定義的內(nèi)部類訪問的局部變量不是編譯時(shí)可確定的字面常量, 又會(huì)怎么樣呢?想要讓這個(gè)局部變量變成編譯時(shí)不可確定的, 只需要將源碼修改如下:
由于使用getString方法的返回值為localVar賦值, 所以在編譯時(shí)期, 編譯器不可確定localVar的值, 必須在運(yùn)行時(shí)執(zhí)行了getString方法之后才能確定它的值。 既然編譯時(shí)不不可確定, 那么像上面那樣的處理就行不通了。 那么在這種情況下, 內(nèi)部類是通過什么機(jī)制訪問方法中的局部變量的呢? 讓我們繼續(xù)反編譯內(nèi)部類的字節(jié)碼:
首先來看它的構(gòu)造方法。 方法的簽名為:
我們只到, 如果不定義構(gòu)造方法, 那么編譯器會(huì)為這個(gè)類自動(dòng)生成一個(gè)無參數(shù)的構(gòu)造方法。 這個(gè)說法在這里就行不通了, 因?yàn)槲覀兛吹剑?這個(gè)內(nèi)部類的構(gòu)造方法又兩個(gè)參數(shù)。 至于第一個(gè)參數(shù), 是指向外部類對(duì)象的引用, 在前面一篇博客中已經(jīng)詳細(xì)的介紹過了, 不明白的可以先看上一篇博客, 這里就不再重復(fù)敘述。這也說明了方法中的內(nèi)部類和類中定義的內(nèi)部類有相同的地方, 既然他們都是內(nèi)部類, 就都持有指向外部類對(duì)象的引用。 我們來分析第二個(gè)參數(shù), 他是String類型的, 和在內(nèi)部類中訪問的局部變量localVar的類型相同。 再看構(gòu)造方法中編號(hào)為6和7的字節(jié)碼指令:
這句話的意思是, 使用構(gòu)造方法的第二個(gè)參數(shù), 為當(dāng)前這個(gè)內(nèi)部類對(duì)象的成員變量賦值, 這個(gè)被賦值的成員變量的名字是 val$localVar 。 由此可見, 編譯器自動(dòng)為內(nèi)部類增加了一個(gè)成員變量, 其實(shí)這個(gè)成員變量就是被訪問的外部類方法中的局部變量。 這個(gè)局部變量在創(chuàng)建內(nèi)部類對(duì)象時(shí), 通過構(gòu)造方法注入。 在調(diào)用構(gòu)造方法時(shí), 編譯器會(huì)默認(rèn)為這個(gè)參數(shù)傳入外部類方法中的局部變量的值。
再看內(nèi)部類中的方法innerMethod中是如何訪問這個(gè)所謂的“局部變量的”。 看innerMethod中的前兩條字節(jié)碼:
這兩條指令的意思是, 訪問成員變量val$localVar的值。 而源代碼中是訪問外部類方法中局部變量的值。 所以, 在這里將編譯時(shí)對(duì)外部類方法中的局部變量的訪問, 轉(zhuǎn)化成運(yùn)行時(shí)對(duì)當(dāng)前內(nèi)部類對(duì)象中成員變量的訪問。 在源代碼層面上, 它的工作方式有點(diǎn)像這樣: (注意, 下面的代碼不符合Java的語(yǔ)法, 只是模擬編譯器的行為)
講到這里, 內(nèi)部類的行為就比較清晰了。 總結(jié)一下就是: 當(dāng)方法中定義的內(nèi)部類訪問的方法局部變量的值, 不是在編譯時(shí)能確定的字面常量時(shí), 編譯器會(huì)為內(nèi)部類增加一個(gè)成員變量, 在運(yùn)行時(shí), 將對(duì)外部類方法中局部變量的訪問。 轉(zhuǎn)換成對(duì)這個(gè)內(nèi)部類成員變量的方法。 這就要求內(nèi)部類中的這個(gè)新增的成員變量和外部類方法中的局部變量具有相同的值。 編譯器通過為內(nèi)部類的構(gòu)造方法增加參數(shù), 并在調(diào)用構(gòu)造器初始化內(nèi)部類對(duì)象時(shí)傳入這個(gè)參數(shù), 來初始化內(nèi)部類中的這個(gè)成員變量的值。 所以, 雖然在源文件中看起來是訪問的外部類方法的局部變量, 其實(shí)運(yùn)行時(shí)訪問的是內(nèi)部類對(duì)象自己的成員變量。
為什么被方法內(nèi)的內(nèi)部類訪問的局部變量必須是final的上面我們講解了, 方法中的內(nèi)部類訪問方法局部變量是怎么實(shí)現(xiàn)的。 那么為什么這個(gè)局部變量必須是final的呢? 我認(rèn)為有以下兩個(gè)原因:
1 當(dāng)局部變量的值為編譯時(shí)可確定的字面常量時(shí)( 如字符串“abc”或整數(shù)1 ), 通過final修飾, 可以實(shí)現(xiàn)類似C語(yǔ)言的編譯時(shí)宏替換功能。 這樣的話, 外部類和內(nèi)部類各自訪問自己的常量池, 各自執(zhí)行各自的字節(jié)碼指令, 看起來就像共同訪問外部類方法中的局部變量。 這樣就可以達(dá)到語(yǔ)義上的一致性。 由于存在內(nèi)部類和外部類中的常量值是一樣的, 并且是不可改變的,這樣就可以達(dá)到數(shù)值訪問的一致性。
2 當(dāng)局部變量的值不是可在編譯時(shí)確定的字面常量時(shí)(比如通過方法調(diào)用為它賦值), 這種情況下, 編譯器給內(nèi)部類增加相同類型的成員變量, 并通過構(gòu)造函數(shù)將外部類方法中的局部變量的值賦給這個(gè)新增的內(nèi)部類成員變量。
如果這個(gè)局部變量是基本數(shù)據(jù)類型時(shí), 直接拷貝數(shù)值給內(nèi)部類成員變量。代碼示例和運(yùn)行時(shí)內(nèi)存布局是這樣的:
![]() 這樣的話, 內(nèi)部類和外部類各自訪問自己的基本數(shù)據(jù)類型的變量,
他們的變量值一樣, 并且不可修改, 這樣就保證了語(yǔ)義上和數(shù)值訪問上的一致性。
如果這個(gè)局部變量是引用數(shù)據(jù)類型時(shí), 拷貝外部類方法中的引用值給內(nèi)部類對(duì)象的成員變量, 這樣的話, 他們就指向了同一個(gè)對(duì)象。 代碼示例和運(yùn)行時(shí)的內(nèi)存布局如下:
![]()
由于這兩個(gè)引用變量指向同一個(gè)對(duì)象, 所以通過引用訪問的對(duì)象的數(shù)據(jù)是一樣的, 由于他們都不能再指向其他對(duì)象(被final修飾), 所以可以保證內(nèi)部類和外部類數(shù)據(jù)訪問的一致性。
|
|