Java8 中 Lambda函數(shù)的 Effectively final 特性——從階乘說(shuō)起

在某乎看到一個(gè)提問(wèn),怎樣用Lambda寫(xiě)階乘窟社?

由于Java的 Effectively final 特性痊末,以下代碼無(wú)法通過(guò)編譯。

Function<Integer, Integer> factorial = null;
factorial = i -> i == 1 ? 1 : factorial .apply(i - 1);

有位答主提供了一個(gè)方案:在構(gòu)造函數(shù)中初始化:

public class Factorial {
    Function<Integer, Integer> factorial = null;

    public Factorial() {
        factorial = i -> i == 1 ? 1 : factorial.apply(i - 1);
    }

    public static void main(String[] args) {
        System.out.println(new Factorial().factorial.apply(5));
    }
}

看起來(lái)和錯(cuò)誤寫(xiě)法差不多内狸,但居然是可用的检眯!但是答主并不知道原理,解釋為可能是構(gòu)造函數(shù)的特殊性昆淡。

僅僅是因?yàn)闃?gòu)造函數(shù)嗎锰瘸?試了下在構(gòu)造代碼塊中,在普通函數(shù)中昂灵,都可以正確初始化避凝。似乎只要lambda函數(shù)是類變量就可以舞萄。

public class Factorial {
    Function<Integer, Integer> factorial = null;

    // 構(gòu)造代碼塊中初始化
    {
        factorial = i -> i == 1 ? 1 : factorial.apply(i - 1);
    }

    // 普通函數(shù)初始化
//    public Function<Integer, Integer> set() {
//        factorial = i -> i == 1 ? 1 : factorial.apply(i - 1);
//        return factorial;
//    }
    public static void main(String[] args) {
        System.out.println(new Factorial().factorial.apply(5));
//        System.out.println(new Factorial().set().apply(10));
    }
}

甚至把lambda定義為static,在static塊中初始化管削,也能通過(guò)編譯倒脓。

但是這是為什么呢?

為什么Java要求 Lambda 函數(shù)中使用的外部變量必須是 Effectively final含思?

可以看這篇回答崎弃,雖然講的是匿名內(nèi)部類,但是原理相通含潘。

簡(jiǎn)單來(lái)講就是:Java支持了有限的閉包饲做,在編譯時(shí)給匿名內(nèi)部類增加了個(gè)構(gòu)造函數(shù),把外部的局部變量復(fù)制了一份到內(nèi)部類里遏弱。

用匿名內(nèi)部類寫(xiě)這樣一份測(cè)試代碼:

public class Func {
    public static void main(String[] args) {
        Integer a = 10, b = 20;
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(a + b);
            }
        };
        runnable.run();
    }
}

反編譯后的結(jié)果:

// Func.class盆均,把兩個(gè)局部變量寫(xiě)為 final
public static void main(String[] var0) {
    final Integer var1 = 10;
    final Integer var2 = 20;
    Runnable var3 = new Runnable() {
        public void run() {
            System.out.println(var1 + var2);
        }
    };
    var3.run();
}
// Func$1.class,通過(guò)構(gòu)造函數(shù)傳入了局部變量
final class Func$1 implements Runnable {
    Func$1(Integer var1, Integer var2) {
        this.val$a = var1;
        this.val$b = var2;
    }

    public void run() {
        System.out.println(this.val$a + this.val$b);
    }
}

通過(guò)偷偷塞構(gòu)造函數(shù)的方式漱逸,傳入了局部變量的一個(gè)拷貝泪姨。

如果允許修改該局部變量的引用,外部修改無(wú)法對(duì)內(nèi)部生效虹脯,內(nèi)部的修改也無(wú)法對(duì)外部生效驴娃,一定程度上會(huì)引起歧義,索性寫(xiě)死為final得了循集。

其他語(yǔ)言是怎么做的唇敞?

但是同為JVM語(yǔ)言的Scala,似乎并沒(méi)有這種限制咒彤。

// Scala源碼,Lambda 函數(shù)中修改number的值疆柔,且生效
def tryAccessingLocalVariable {
  var number = 1
  println(number)

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

  lambda.apply()
  println(number)
}
// 編譯后,用 IntRef 包裝了 number
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 = 2;
        Predef..MODULE$.println(BoxesRunTime.boxToInteger(this.number$2.elem));
    }

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

Scala是通過(guò) IntRef 包裝以后傳入的镶柱,修改時(shí)通過(guò)IntRef.elem = 2;的方式旷档,并未修改IntRef的引用,實(shí)際IntRef還是final的歇拆。

手動(dòng)繞過(guò)外部變量不允許修改的限制

和Scala類似鞋屈,在Java里,我們給變量加個(gè)包裝即可故觅,只不過(guò)需要手動(dòng)添加厂庇。據(jù)說(shuō)JDK里有很多代碼就是這樣干的。

int a[] = {0};
Runnable runnable = () -> a[0]++;

匿名函數(shù)/Lambda函數(shù)對(duì)類變量的處理

匿名函數(shù)和Lambda函數(shù)在這方面有相似的特性输吏,所以把最開(kāi)始那個(gè)階乘代碼寫(xiě)成匿名函數(shù)的方式并反編譯(Lambda反編譯看不到細(xì)節(jié)):

// 源碼
public class Factorial {
    static Function<Integer, Integer> factorial = null;

    static {
        factorial = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer i) {
                return i == 0 ? 1 : i * factorial.apply(i - 1);
            }
        };
    }
    public static void main(String[] args) {
        System.out.println(Factorial.factorial.apply(10));
    }
}
// 匿名函數(shù)反編譯后的類
class Factorial$1 implements Function<Integer, Integer> {
    Factorial$1(Factorial var1) {
        this.this$0 = var1;
    }

    public Integer apply(Integer var1) {
        return var1 == 0 ? 1 : var1 * (Integer)Factorial.factorial.apply(var1 - 1);
    }
}

編譯器在匿名類的構(gòu)造函數(shù)里把外部對(duì)象給傳了進(jìn)來(lái)权旷,和內(nèi)部類非常相似。

所以明顯的贯溅,在這種情況下拄氯,看起來(lái)似乎繞過(guò)了 Effectively final 特性躲查,實(shí)際是匿名函數(shù)/Lambda函數(shù)持有了外部對(duì)象的引用。

為什么JDK要用如此別扭的方式译柏,而不是像Scala那樣支持完整的閉包呢镣煮?

openJDK給出的回答是,類似Scala這種處理方式鄙麦,在多線程下會(huì)有問(wèn)題怎静。相比它的好處,它帶來(lái)的問(wèn)題似乎更嚴(yán)重黔衡。

Lamabda 階乘的另一種寫(xiě)法

由于數(shù)組也是特殊的對(duì)象,所以還可以這樣寫(xiě):

Function<Integer, Integer>[] funcs = new Function [1];
funcs[0] = i -> i == 0 ? 1 : i * factorial.apply(i - 1);

更簡(jiǎn)單直觀腌乡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盟劫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子与纽,更是在濱河造成了極大的恐慌侣签,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件急迂,死亡現(xiàn)場(chǎng)離奇詭異影所,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)僚碎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門猴娩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人勺阐,你說(shuō)我怎么就攤上這事卷中。” “怎么了渊抽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵蟆豫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我懒闷,道長(zhǎng)十减,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任愤估,我火速辦了婚禮帮辟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灵疮。我一直安慰自己织阅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布震捣。 她就那樣靜靜地躺著荔棉,像睡著了一般闹炉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上润樱,一...
    開(kāi)封第一講書(shū)人閱讀 52,874評(píng)論 1 314
  • 那天渣触,我揣著相機(jī)與錄音,去河邊找鬼壹若。 笑死嗅钻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的店展。 我是一名探鬼主播养篓,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赂蕴!你這毒婦竟也來(lái)了柳弄?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤概说,失蹤者是張志新(化名)和其女友劉穎碧注,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體糖赔,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萍丐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了放典。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逝变。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖奋构,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情声怔,我是刑警寧澤态贤,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布醋火,位于F島的核電站,受9級(jí)特大地震影響芥驳,放射性物質(zhì)發(fā)生泄漏柿冲。R本人自食惡果不足惜兆旬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宿饱,春花似錦熏瞄、人聲如沸谬以。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)邮丰。三九已至,卻和暖如春铭乾,著一層夾襖步出監(jiān)牢的瞬間剪廉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工炕檩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妈经,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓捧书,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親骤星。 傳聞我的和親對(duì)象是個(gè)殘疾皇子经瓷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361