Java中的final參數/變數+匿名內部類的位元組碼分析,以及Java 8中的使用__java

來源:互聯網
上載者:User

在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;    }






聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.