在Java 8之前一個常識就是如果要在方法中定義一個匿名內部類並使用該方法內的局部變數(包括參數),需要使用final關鍵字修飾。網上也有很多對這種機制的解釋和說明,但是大部分都是一種抽象的認識。如果能夠分析一下位元組碼,這個問題其實很清楚。
顯然的一個事實是局部變數(稱為變數a)是儲存在棧幀的局部變數表中的(引用或基本類型),這裡把定義匿名內部類的方法稱為方法A,匿名內部類的中使用這個局部變數的方法(稱為方法B)經過編譯後儲存在class檔案的方法表中,方法體儲存在Code屬性中。運行時經過類載入,驗證儲存到方法區中。在執行匿名內部類中,JVM建立的是方法B的棧幀。它顯然和方法A的棧幀沒有直接的關係,甚至方法B執行時,方法A的棧幀已經被回收了,那JVM如何保證方法B能夠正確的使用變數a指向的值呢。
下面就通過查看位元組碼來看看Java編譯器和JVM是如何做到這一點的,基於Oracle JDK8編譯; 1. 位元組碼分析:
外圍類:FinalParameter
定義一個派生自A的匿名內部類,其中使用方法testFinal的局部變數a和參數bytes;
public final class FinalParameter { interface A { void test(); } private void testFinal(final byte[] bytes) { final int a = 10; new A() { @Override public void test() { System.out.println(a + " " + bytes.length); } }; }}
根據內部類命名規則我們知道這個匿名內部類的類名是FinalParameter$1; 使用javap -verbose FinalParameter$1,查看該類的位元組碼,Unix環境下注意不要忘了轉義"$"。
我截取了欄位表部分:
final int val$a; descriptor: I flags: ACC_FINAL, ACC_SYNTHETIC final byte[] val$bytes; descriptor: [B flags: ACC_FINAL, ACC_SYNTHETIC final com.jvm.showByteCode.FinalParameter this$0; descriptor: Lcom/jvm/showByteCode/FinalParameter; flags: ACC_FINAL, ACC_SYNTHETIC
可以看到編譯器為我們添加了3個欄位,分別是:
val$a,val$bytes,this$0;
其中this$0是指向外圍類對象執行個體的引用,而前兩個正是從外圍類方法局部變數bytes和a產生而來的,並且它們是final的。
再來看看FinalParameter$1類的<init>()方法:
描述符:
descriptor: (Lcom/jvm/showByteCode/FinalParameter;I[B)V
Code屬性(方法實現):
Code: stack=2, locals=4, args_size=4 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Lcom/jvm/showByteCode/FinalParameter; 5: aload_0 6: iload_2 7: putfield #2 // Field val$a:I 10: aload_0 11: aload_3 12: putfield #3 // Field val$bytes:[B 15: aload_0 16: invokespecial #4 // Method java/lang/Object."<init>":()V 19: return
外圍類中testFinal方法中調用匿名內部類的部分:
10: invokespecial #3 // Method com/jvm/showByteCode/FinalParameter$1."<init>":(Lcom/jvm/showByteCode/FinalParameter;I[B)V
這裡我們就可以清楚看到以下事實:
(1)<init>方法有4個參數,其中第一個是this,另外三個分別類型分別FinalParameter,int,byte數組;
(2)它們參數被賦值給了對應的final欄位(this$0,val$a,val$bytes);
(3)外圍類將當前執行個體對象引用,a數值,bytes數組引用傳遞給了FinalParameter$1的<init>()方法;
因此,我們可以知道,在匿名內部類中使用外圍類方法的局部變數,實際上是使用對應值(引用)的final成員變數。
內部類中使用的是該局部變數的副本,因此如果要在語義上保證局部變數和副本的一致性,就應當使用final來保證該局部變數不變。
2. Java 8中“改進”: 下段代碼通過了基於JDK8的編譯,但是如果將注釋去掉,仍然不能通過編譯,看來只是在寫法上“省力”了。 也就是說,Java中對於內部類使用局部變數的設計思想還要要保證這兩個變數(一是局部變數,而是內部類的成員變數)一致性; 如果你不想保證這樣的一致性,我覺得可以有以下方案: (1)可以建立一個抽象類別,添加一個構造器,通過構造器傳入局部變數的值; (2)編寫一個一個Factory 方法(或工廠類),局部變數傳入Factory 方法(Factory 方法的對應參數是final的)返回一個內部類執行個體; 這樣你就不用給局部變數設定為final; 因此,Java設計團隊的設計思想是程式員在內部類中使用了局部變數,這種情況應該預設為需要保持兩個變數的一致性。
private void testFinal(byte[] bytes) { int a = 10; new A() { @Override public void test() { System.out.println(a + " " + bytes.length); } };// a = 11;// bytes = null; }