【轉(zhuǎn)】為什么必須是final的呢?

轉(zhuǎn)自: http://cuipengfei.me/blog/2013/06/22/why-does-it-have-to-be-final/


一個謎團

如果你用過類似guava這種“偽函數(shù)式編程”風(fēng)格的library的話疏唾,那下面這種風(fēng)格的代碼對你來說應(yīng)該不陌生:

public void tryUsingGuava() {
    final int expectedLength = 4;
    Iterables.filter(Lists.newArrayList("123", "1234"), new Predicate<String>() {
        @Override
        public boolean apply(String str) {
            return str.length() == expectedLength;
        }
    });
}

這段代碼對一個字符串的list進行過濾禁偎,從中找出長度為4的字符串∩按看起來很是平常惜互,沒什么特別的利花。

但是科侈,聲明expectedLength時用的那個final看起來有點扎眼,把它去掉試試:

error: local variable expectedLength is accessed from within inner class; needs to be declared final

結(jié)果Java編譯器給出了如上的錯誤炒事,看起來匿名內(nèi)部類只能夠訪問final的局部變量臀栈。但是,為什么呢挠乳?其他的語言也有類似的規(guī)定嗎权薯?

在開始用其他語言做實驗之前我們先把問題簡化一下,不要再帶著guava了睡扬,我們?nèi)コ粼胍裘蓑迹褑栴}歸結(jié)為:

為什么Java中的匿名內(nèi)部類只可以訪問final的局部變量呢?其他語言中的匿名函數(shù)也有類似的限制嗎卖怜?


Scala中有類似的規(guī)定嗎屎开?

  def tryAccessingLocalVariable {
    var number = 123
    println(number)

    var lambda = () => {
      number = 456
      println(number)
    }

    lambda.apply()
    println(number)
  }

上面的Scala代碼是合法的,number變量是聲明為var的马靠,不是val(類似于Java中的final)奄抽。而且在匿名函數(shù)中可以修改number的值。

看來Scala中沒有類似的規(guī)定甩鳄。


C#中有類似的規(guī)定嗎逞度?

public void tryUsingLambda ()
{
  int number = 123;
  Console.WriteLine (number);

  Action action = () => {
      number = 456;
      Console.WriteLine (number);
  };

  action ();
  Console.WriteLine (number);
}

這段C#代碼也是合法的,number這個局部變量在lambda表達式內(nèi)外都可以訪問和賦值妙啃。

看來C#中也沒有類似的規(guī)定档泽。


分析謎團

三門語言中只有Java有這種限制,那我們分析一下吧揖赴。先來看一下Java中的匿名內(nèi)部類是如何實現(xiàn)的:

先定義一個接口:

public interface MyInterface {
    void doSomething();
}

然后創(chuàng)建這個接口的匿名子類:

public class TryUsingAnonymousClass {
    public void useMyInterface() {
        final Integer number = 123;
        System.out.println(number);

        MyInterface myInterface = new MyInterface() {
            @Override
            public void doSomething() {
                System.out.println(number);
            }
        };
        myInterface.doSomething();

        System.out.println(number);
    }
}

這個匿名子類會被編譯成一個單獨的類馆匿,反編譯的結(jié)果是這樣的:

class TryUsingAnonymousClass$1
        implements MyInterface {
    private final TryUsingAnonymousClass this$0;
    private final Integer paramInteger;

    TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {
        this.this$0 = this$0;
        this.paramInteger = paramInteger;
    }

    public void doSomething() {
        System.out.println(this.paramInteger);
    }
}

可以看到名為number的局部變量是作為構(gòu)造方法的參數(shù)傳入匿名內(nèi)部類的(以上代碼經(jīng)過了手動修改,真實的反編譯結(jié)果中有一些不可讀的命名)燥滑。

如果Java允許匿名內(nèi)部類訪問非final的局部變量的話渐北,那我們就可以在TryUsingAnonymousClass$1中修改paramInteger,但是這不會對number的值有影響突倍,因為它們是不同的reference。

這就會造成數(shù)據(jù)不同步的問題盆昙。

所以羽历,謎團解開了:Java為了避免數(shù)據(jù)不同步的問題,做出了匿名內(nèi)部類只可以訪問final的局部變量的限制淡喜。

但是秕磷,新的謎團又出現(xiàn)了:


Scala和C#為什么沒有類似的限制呢?它們是如何處理數(shù)據(jù)同步問題的呢炼团?

上面出現(xiàn)過的那段Scala代碼中的lambda表達式會編譯成這樣:

public final class TryUsingAnonymousClassInScala$$anonfun$1 extends AbstractFunction0.mcV.sp
        implements Serializable {
    public static final long serialVersionUID = 0L;
    private final IntRef number$2;

    public final void apply() {
        apply$mcV$sp();
    }

    public void apply$mcV$sp() {
        this.number$2.elem = 456;
        Predef..MODULE$.println(BoxesRunTime.boxToInteger(this.number$2.elem));
    }

    public TryUsingAnonymousClassInScala$$anonfun$1(TryUsingAnonymousClassInScala $outer, IntRef number$2) {
        this.number$2 = number$2;
    }
}

可以看到number也是通過構(gòu)造方法的參數(shù)傳入的澎嚣,但是與Java的不同是這里的number不是直接傳入的疏尿,是被IntRef包裝了一層然后才傳入的。對number的值修改也是通過包裝類進行的:this.number$2.elem = 456;

這樣就保證了lambda表達式內(nèi)外訪問到的是同一個對象易桃。

再來看看C#的處理方式褥琐,反編譯一下,發(fā)現(xiàn)C#編譯器生成了如下的一個類:

private sealed class <tryUsingLambda>c__AnonStorey0
{
  internal int number;

  internal void <>m__0 ()
  {
      this.number = 456;
      Console.WriteLine (this.number);
  }
}

把number包裝在這個類內(nèi)晤郑,這樣就保證了lambda表達式內(nèi)外使用的都是同一個number敌呈,即便重新賦值也可以保證內(nèi)外部的數(shù)據(jù)是同步的。


小結(jié)

Scala和C#的編譯器通過把局部變量包裝在另一個對象中造寝,來實現(xiàn)lambda表達式內(nèi)外的數(shù)據(jù)同步磕洪。

而Java的編譯器由于未知的原因(懷疑是為了圖省事兒?)沒有做包裝局部變量這件事兒诫龙,于是就只好強制用戶把局部變量聲明為final才能在匿名內(nèi)部類中使用來避免數(shù)據(jù)不同步的問題析显。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市签赃,隨后出現(xiàn)的幾起案子谷异,更是在濱河造成了極大的恐慌,老刑警劉巖姊舵,帶你破解...
    沈念sama閱讀 211,496評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晰绎,死亡現(xiàn)場離奇詭異,居然都是意外死亡括丁,警方通過查閱死者的電腦和手機荞下,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,187評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來史飞,“玉大人尖昏,你說我怎么就攤上這事」棺剩” “怎么了抽诉?”我有些...
    開封第一講書人閱讀 157,091評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吐绵。 經(jīng)常有香客問我迹淌,道長,這世上最難降的妖魔是什么己单? 我笑而不...
    開封第一講書人閱讀 56,458評論 1 283
  • 正文 為了忘掉前任唉窃,我火速辦了婚禮,結(jié)果婚禮上纹笼,老公的妹妹穿的比我還像新娘纹份。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,542評論 6 385
  • 文/花漫 我一把揭開白布蔓涧。 她就那樣靜靜地躺著件已,像睡著了一般。 火紅的嫁衣襯著肌膚如雪元暴。 梳的紋絲不亂的頭發(fā)上篷扩,一...
    開封第一講書人閱讀 49,802評論 1 290
  • 那天,我揣著相機與錄音昨寞,去河邊找鬼瞻惋。 笑死,一個胖子當(dāng)著我的面吹牛援岩,可吹牛的內(nèi)容都是我干的歼狼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,945評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼享怀,長吁一口氣:“原來是場噩夢啊……” “哼羽峰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起添瓷,我...
    開封第一講書人閱讀 37,709評論 0 266
  • 序言:老撾萬榮一對情侶失蹤梅屉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鳞贷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坯汤,經(jīng)...
    沈念sama閱讀 44,158評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,502評論 2 327
  • 正文 我和宋清朗相戀三年搀愧,在試婚紗的時候發(fā)現(xiàn)自己被綠了惰聂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,637評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡咱筛,死狀恐怖搓幌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迅箩,我是刑警寧澤溉愁,帶...
    沈念sama閱讀 34,300評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站饲趋,受9級特大地震影響拐揭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奕塑,卻給世界環(huán)境...
    茶點故事閱讀 39,911評論 3 313
  • 文/蒙蒙 一堂污、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爵川,春花似錦敷鸦、人聲如沸息楔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,744評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至圃泡,卻和暖如春碟案,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颇蜡。 一陣腳步聲響...
    開封第一講書人閱讀 31,982評論 1 266
  • 我被黑心中介騙來泰國打工价说, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人风秤。 一個月前我還...
    沈念sama閱讀 46,344評論 2 360
  • 正文 我出身青樓鳖目,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缤弦。 傳聞我的和親對象是個殘疾皇子领迈,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,500評論 2 348

推薦閱讀更多精彩內(nèi)容