Java匿名類遇上final

時(shí)間: 2018/10/19

Content

  1. final的普通語義

  2. final遇見內(nèi)部類

  3. 閉包

  4. 內(nèi)存泄漏

    ?

1. final的普通語義

關(guān)于Java中final關(guān)鍵字的常規(guī)語義就是表明其修飾的對(duì)象是不可變的, 被修飾的對(duì)象通常有. 值變量,引用變量,類,函數(shù)。此處需要注意的是,如果final修飾的是引用變量,那么引用變量的值(地址)不可變,但是引用變量值對(duì)應(yīng)的對(duì)象實(shí)可以變的。

分別介紹一下修飾不同對(duì)象的情況:

  • 1). 值變量
  • 2). 引用變量
  • 3). 類
  • 4). 方法
// 1)
final int a = 1;
a = 1; // 這個(gè)語句會(huì)報(bào)錯(cuò),不允許修改

// 2)
final Map map = new HashMap();
map.put("key", "value"); // map值(地址)對(duì)應(yīng)對(duì)象(在堆)可以被修改,一般認(rèn)為對(duì)象被生成以后,其地址就是確定了
map = new HashMap(); // 會(huì)報(bào)錯(cuò), map值(地址)不允許修改

// 3)
final class Cls{
// .....
}
class SubCls extends Cls{ //編譯報(bào)錯(cuò),Cls不能為繼承
  // ....
}

// 4)
class Parent{
  public final void method(){
    // ...
  }
}   
class Child extends Parent{
  public void mehtod(){ // 編譯報(bào)錯(cuò),不允許覆蓋
    // ..
  }
}

2. final遇見內(nèi)部類

Java中要求如果方法中定義的中類如果引用方法中的局部變量,那要要求局部變量必須要用final修飾(JDK8中已經(jīng)不需要,但是本質(zhì)也是和final類似——只讀),實(shí)例代碼如下:

interface Inner{
  void method();
}

class Outer{
    public Inner createInner(){
        final int a = 12;
        final Map map = new HashMap();
        Inner inner = new Inner(){
            public void method(){
                int b = a + 1;
                System.out.println(" in Inner, b=" + b);
                map.put("innerKey", "innerValue");
            }
        };
        System.out.println("in Outer, createInner finish!");
        return inner;
    }

    public static void main(String []args){
        Inner inner = new Outer().createInner();
        inner.method();
    }
}

輸出如下

in Outer, createInner finish!
in Inner, b=13

Note: 上述代碼僅僅是展示使用,其中createInner()方法中的map變量是存在內(nèi)存泄漏的,因?yàn)橥饨鐭o法訪問他,但是卻會(huì)被一致持有。關(guān)于內(nèi)存泄漏的問題,通過查看上述代碼便后的class文件的內(nèi)容即可發(fā)現(xiàn)。

上述文件編譯后,生成了三個(gè)文件

  • Inner.class

  • Outer.class

  • Outer$1.class

    打開Outer$1.class可以看到如下內(nèi)容:

  class Outer$1 implements Inner {
      Outer$1(Outer this$0, int var2, Map var3) {
          this.this$0 = this$0;
          this.val$a = var2;
          this.val$map = var3;
      }
      
      public void method() {
          int b = this.val$a + 1;
          System.out.println(" in Inner, b=" + b);
          this.val$map.put("innerKey", "innerValue");
      }
  }

可以看到編譯后的內(nèi)容,Inner匿名類擁有另一個(gè)帶有三個(gè)參數(shù)的構(gòu)造方法,

  • Outer this$0: 也就是擁有了Outer(外部類)當(dāng)前對(duì)象的一個(gè)引用,所以我們Inner的子類中,可以通過Outer.this訪問外部Outer類的當(dāng)前實(shí)例。
  • var2: 此處應(yīng)該為Outer createInner()方法中的局部變量a
  • var3: 此處應(yīng)該為Outer createInner()方法中的局部變量Map

通過上述編譯后的代碼,我們大概可以明白為什么匿名類可以訪問其外部數(shù)據(jù)的原因,接下來我們可以討論一下為什么要對(duì)createInner()中的局部變量a, map用final進(jìn)行修飾。

網(wǎng)絡(luò)上有很多人說是生命周期的問題,但是我覺得不是這個(gè)原因,也覺得不存在生命周期的問題(歡迎討論)。

為了簡(jiǎn)化表述,以下將Inner匿名類里面的a表述為Inner().a, 將createInner()方法中的a表示為 createInner.a.

通過編譯后的代碼可以看出來,Inner().acreateInner.a不是同一個(gè)對(duì)象(在內(nèi)存中不是同一個(gè)), 同樣的兩個(gè)map(值,存在于堆)在內(nèi)存中也是不同的,但是兩個(gè)map的都指向了堆上的同一個(gè)HashMap對(duì)象。理論上我們是可以重新設(shè)置Inner().aInner().map的值的,但是java編譯器并不允許這樣做, 具體原因我認(rèn)為可能是如下原因:

在匿名類內(nèi)部訪問外面的變量看起來是一個(gè)很正常的需求,而且直觀看起來應(yīng)該是同一個(gè)東西。但是在方法調(diào)用結(jié)束以后局部變量會(huì)被銷毀(棧里面的內(nèi)容,也就是createInner.a, createInner.map。如果是同一個(gè)東西的話,那么意味著jvm在方法調(diào)用結(jié)束以后還不能銷毀這些局部變量,需要將這些局部變量的生命周期保持到和Inner一樣長(zhǎng),這樣讓jvm的實(shí)現(xiàn)起可能會(huì)更為復(fù)雜(提升這些變量的生命周期)。

所以,為了實(shí)現(xiàn)在Inner中可以訪問createInner()中的a, map,同時(shí)他們看起來和createInner()中的一樣(一致),并且避免JVM對(duì)對(duì)象生命周期的管理過于復(fù)雜,采用了一個(gè)中折中的辦法:

  1. 將被用到的變量作為Inner的構(gòu)造函數(shù)參數(shù)傳入并在Inner內(nèi)部設(shè)置對(duì)應(yīng)的實(shí)例(private)。
  2. createInner().a, createInner().map設(shè)置為final,并且匿名類類部不可以修改對(duì)應(yīng)實(shí)例屬性的值,保證一致性。

通過上述的 1中,可以很自然實(shí)現(xiàn)在Inner中很自然的訪問createInner中局部變量的值;由于Inner中使用的變量實(shí)際上和外部函數(shù)中的局部變量是不一樣的,通過上述2可以保證他們一致(都不允許修改了,肯定一致), 否則開發(fā)者在內(nèi)部修改值,但是卻不會(huì)影響到外面的局部變量,這會(huì)讓人困惑(天然看起來應(yīng)該是一個(gè)東西啊,但是卻不能一起變化)。

3. 閉包

此處引出了Java對(duì)閉包的支持,其實(shí)Java目前是支持了閉包的,匿名類就是一個(gè)典型的例子。將自由變量(createInner.a,createInner.map)封裝到Inner中,但是Java的閉包確實(shí)有條件的閉包,因?yàn)镴ava只實(shí)現(xiàn)了capture-by-value, 只是把局部變量的值copy到了匿名類中, 沒有實(shí)現(xiàn)capture-by-reference。如果是capture-by-reference的實(shí)現(xiàn)方式,可能需要將局部變量提升到對(duì)象中(也就是講局部變量的生命周期延長(zhǎng),變?yōu)楹?code>Inner類一樣長(zhǎng), 那么在createInner()執(zhí)行完畢以后,就不會(huì)銷毀 a, map了)。

關(guān)于閉包的定義:Ruby之父松本行弘在《代碼的未來》一書中解釋的最好:閉包就是把函數(shù)以及變量包起來,使得變量的生存周期延長(zhǎng)。

此處有一個(gè)系列的參考文章關(guān)于``Javascript```中閉包的內(nèi)容,圖文并茂。深入理解javascript原型和閉包]

4. 內(nèi)存泄漏

Java中并沒有真正的實(shí)現(xiàn)延長(zhǎng)生命周期, 但是間接實(shí)現(xiàn)了createInner.map的生命周期,因?yàn)?code>Inner.map是一個(gè)對(duì)實(shí)際的HashMap()(位于堆中)對(duì)象的引用, 所以在createInner()方法中創(chuàng)建,但是卻不會(huì)在該方法執(zhí)行以后被GC回收, 該對(duì)象的生命周期和其創(chuàng)建Inner實(shí)例一樣長(zhǎng)。在本例中的代碼的內(nèi)存泄漏就由此而生。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容