非捕获Lambda的实例
大約一個(gè)月前,我在Java 8的lambda表達(dá)式框架下總結(jié)了Brian Goetz的觀點(diǎn) 。 目前,我正在研究有關(guān)默認(rèn)方法的文章,令我驚訝的是,我又回到了Java處理lambda表達(dá)式的方式。 這兩個(gè)功能的交集可能會產(chǎn)生微妙但令人驚訝的效果,我想討論一下。
總覽
為了使這一點(diǎn)更有趣,我將以一個(gè)示例開頭,該示例將以我的個(gè)人WTF達(dá)到頂峰? 時(shí)刻。 完整的示例可以在專用的GitHub項(xiàng)目中找到 。
然后,我們將看到有關(guān)此意外行為的解釋,并最終得出一些預(yù)防錯(cuò)誤的結(jié)論。
例
這里有個(gè)例子……它不是那么簡單或抽象,因?yàn)槲蚁M@示這種情況的相關(guān)性。 但是從某種意義上說,它仍然只是一個(gè)示例,它僅暗示可能實(shí)際上會做一些有用的事情的代碼。
功能界面
假設(shè)對于在構(gòu)建期間結(jié)果已經(jīng)存在的情況,我們需要對Future接口進(jìn)行特殊化。
我們決定通過創(chuàng)建一個(gè)接口ImmediateFuture來實(shí)現(xiàn)此目的,該接口get()使用默認(rèn)方法實(shí)現(xiàn)除get()之外的所有功能。 這導(dǎo)致功能界面 。
您可以在此處查看源代碼。
一個(gè)工廠
接下來,我們實(shí)現(xiàn)FutureFactory 。 它可能創(chuàng)建各種期貨,但肯定會創(chuàng)建我們的新子類型。 它是這樣的:
未來工廠
/*** Creates a new future with the default result.*/ public static Future<Integer> createWithDefaultResult() {ImmediateFuture<Integer> immediateFuture = () -> 0;return immediateFuture; }/*** Creates a new future with the specified result.*/ public static Future<Integer> createWithResult(Integer result) {ImmediateFuture<Integer> immediateFuture = () -> result;return immediateFuture; }創(chuàng)造未來
最后,我們使用工廠創(chuàng)建一些期貨并將其收集在一組中:
創(chuàng)建實(shí)例
public static void main(String[] args) {Set<Future<?>> futures = new HashSet<>();futures.add(FutureFactory.createWithDefaultResult());futures.add(FutureFactory.createWithDefaultResult());futures.add(FutureFactory.createWithResult(42));futures.add(FutureFactory.createWithResult(63));System.out.println(futures.size()); }WTF ?!
運(yùn)行程序。 控制臺會說...
4? 不。 3。
WTF ?!
Lambda表達(dá)式的評估
那么這是怎么回事? 那么,與有關(guān)lambda表達(dá)式的評估一些背景知識,這其實(shí)并不奇怪。 如果您不太熟悉Java的實(shí)現(xiàn)方式,那么現(xiàn)在是趕上Java的好時(shí)機(jī)。 這樣做的一種方法是觀看Brian Goetz的演講“ Java中的Lambdas:深入了解”或閱讀我的摘要 。
Lambda表達(dá)式的實(shí)例
理解這種行為的關(guān)鍵在于,事實(shí)是JRE不保證如何將lambda表達(dá)式轉(zhuǎn)換為相應(yīng)接口的實(shí)例。 讓我們看一下Java語言規(guī)范對此事的看法:
15.27.4。 Lambda表達(dá)式的運(yùn)行時(shí)評估
[…]分配并初始化具有以下屬性的類的新實(shí)例,或者引用具有以下屬性的類的現(xiàn)有實(shí)例。
[…類的屬性–這里不足為奇…]這些規(guī)則旨在通過以下方式為Java編程語言的實(shí)現(xiàn)提供靈活性:
- 不必在每次評估中分配一個(gè)新對象。
- 由不同的lambda表達(dá)式產(chǎn)生的對象不必屬于不同的類(例如,如果主體相同)。
- 評估產(chǎn)生的每個(gè)對象不必屬于同一類(例如,可以內(nèi)聯(lián)捕獲的局部變量)。
- 如果“現(xiàn)有實(shí)例”可用,則無需在先前的lambda評估中創(chuàng)建它(例如,可能在封閉類的初始化期間分配了它)。
JLS,Java SE 8版,§15.27.4
在其他優(yōu)化中,這顯然使JRE可以返回相同的實(shí)例,以重復(fù)評估lambda表達(dá)式。
非捕獲Lambda表達(dá)式的實(shí)例
請注意,在上面的示例中,表達(dá)式不捕獲任何變量。 因此,它永遠(yuǎn)不會因評估而改變。 而且由于lambda并非設(shè)計(jì)為具有狀態(tài),因此不同的評估在其生命周期中也無法“分散”。 因此,一般而言,沒有充分的理由來創(chuàng)建多個(gè)不捕獲的lambda實(shí)例,因?yàn)樗鼈冊谡麄€(gè)生命周期中都完全相同。 這樣可以使優(yōu)化始終返回相同的實(shí)例。
(將其與捕獲某些變量的lambda表達(dá)式進(jìn)行對比。對此表達(dá)式的直接評估是創(chuàng)建一個(gè)將捕獲的變量作為字段的類。然后,每個(gè)單個(gè)評估都必須創(chuàng)建一個(gè)新實(shí)例,將實(shí)例存儲在其字段中這些情況顯然并不完全相同。)
這就是上面代碼中發(fā)生的事情。 () -> 0是一個(gè)不捕獲的lambda表達(dá)式,因此每個(gè)評估都返回相同的實(shí)例。 因此,對createWithDefaultResult()每次調(diào)用都是如此。
但是,請記住,這僅適用于當(dāng)前安裝在我的計(jì)算機(jī)上的JRE版本(用于Win 64的Oracle 1.8.0_25-b18)。 您的可以有所不同,下一個(gè)gal也可以如此等等。
得到教訓(xùn)
因此,我們了解了為什么會這樣。 盡管這很有意義,但我仍然會說這種行為并不明顯,因此并不是每個(gè)開發(fā)人員都期望的。 這是產(chǎn)生錯(cuò)誤的溫床,因此讓我們嘗試分析情況并從中學(xué)習(xí)一些東西。
使用默認(rèn)方法進(jìn)行子類型化
可以說,意外行為的根本原因是如何完善Future的決定。 為此,我們擴(kuò)展了另一個(gè)接口,并使用默認(rèn)方法實(shí)現(xiàn)了部分功能。 僅剩一個(gè)未實(shí)現(xiàn)的方法, ImmediateFuture成為了一個(gè)啟用lambda表達(dá)式的功能接口。
另外, ImmediateFuture可以是抽象類。 這樣可以防止工廠意外返回相同的實(shí)例,因?yàn)樗荒苁褂胠ambda表達(dá)式。
關(guān)于抽象類和默認(rèn)方法的討論不容易解決,因此我在這里不嘗試這樣做。 但是,我很快將發(fā)布有關(guān)默認(rèn)方法的文章,并且我打算再講一遍。 可以說,在做出決定時(shí)應(yīng)考慮此處提出的案例。
工廠中的Lambda
由于lambda的引用相等性不可預(yù)測,因此工廠方法應(yīng)仔細(xì)考慮使用它們來創(chuàng)建實(shí)例。 除非方法的合同明確允許不同的調(diào)用返回相同的實(shí)例,否則應(yīng)完全避免使用它們。
我建議在此禁令中包括捕獲lambda。 (對我而言)一點(diǎn)也不清楚,在什么情況下同一實(shí)例可以在將來的JRE版本中重用。 一種可能的情況是,JIT發(fā)現(xiàn)緊密的循環(huán)創(chuàng)建了總是(或至少經(jīng)常)返回同一實(shí)例的供應(yīng)商。 通過用于不捕獲lambda的邏輯,重用同一供應(yīng)商實(shí)例將是有效的優(yōu)化。
匿名類與Lambda表達(dá)式
注意匿名類和lambda表達(dá)式的不同語義。 前者保證創(chuàng)建新實(shí)例,而后者則不能。 為了繼續(xù)該示例,以下createWithDefaultResult()將導(dǎo)致futures –大小為4的集合:
匿名類的替代實(shí)現(xiàn)
public static Future<Integer> createWithDefaultResult() {ImmediateFuture<Integer> immediateFuture = new ImmediateFuture<Integer>() {@Overridepublic Integer get() throws InterruptedException, ExecutionException {return 0;}};return immediateFuture; }這尤其令人不安,因?yàn)樵S多IDE允許從匿名接口實(shí)現(xiàn)到lambda表達(dá)式的自動轉(zhuǎn)換,反之亦然。 由于兩者之間存在細(xì)微的差異,這種看似純粹的句法轉(zhuǎn)換會帶來細(xì)微的行為變化。 (我最初并不了解。)
如果您最終遇到了這種情況,并選擇使用匿名類,請確保明顯記錄您的決定! 不幸的是,似乎沒有辦法阻止Eclipse對其進(jìn)行任何轉(zhuǎn)換(例如,如果將轉(zhuǎn)換作為保存操作啟用),這也會刪除匿名類中的所有注釋。
最終的選擇似乎是一個(gè)(靜態(tài))嵌套類。 我知道沒有IDE敢將其轉(zhuǎn)換為lambda表達(dá)式,因此這是最安全的方法。 盡管如此,仍需要對其進(jìn)行記錄,以防止下一個(gè)Java-8狂熱分子(確實(shí)像您一樣)出現(xiàn)并加緊您的仔細(xì)考慮。
功能接口標(biāo)識
當(dāng)您依賴功能接口的標(biāo)識時(shí)要小心。 始終考慮是否有可能,無論您在何處獲得這些實(shí)例,都可能反復(fù)將您交給同一個(gè)實(shí)例。
但這當(dāng)然是模糊的,幾乎沒有什么具體的結(jié)果。 首先,所有其他接口都可以簡化為功能接口。 這實(shí)際上就是我選擇Future的原因-我想舉個(gè)例子,不要立即尖叫瘋狂的Lambda狗屎! 其次,這會使您很快變得偏執(zhí)。
因此,請不要過分考慮-記住這一點(diǎn)。
保證行為
最后但并非最不重要的一點(diǎn)(這始終是正確的,但值得在此重復(fù)):
不要依靠無證的行為!
JLS不保證每個(gè)lambda評估都返回一個(gè)新實(shí)例(如上面的代碼所示)。 但這并不能保證觀察到的行為,即未捕獲的lambda始終由同一實(shí)例表示。 因此,不要編寫依賴于任何一個(gè)的代碼。
不過,我必須承認(rèn)這是一個(gè)艱難的過程。 認(rèn)真地說,誰在使用某些功能之前先看過它們的JLS? 我當(dāng)然不會。
反射
我們已經(jīng)看到Java不能保證所評估的lambda表達(dá)式的身份。 盡管這是一個(gè)有效的優(yōu)化,但它可能會產(chǎn)生令人驚訝的效果。 為了防止這種情況引入細(xì)微的錯(cuò)誤,我們派生了以下準(zhǔn)則:
- 使用默認(rèn)方法部分實(shí)現(xiàn)接口時(shí)要小心。
- 不要在工廠方法中使用lambda表達(dá)式。
- 當(dāng)身份重要時(shí),請使用匿名類或更好的內(nèi)部類。
- 依賴功能接口的標(biāo)識時(shí)要小心。
- 最后, 不要依賴未記錄的行為!
翻譯自: https://www.javacodegeeks.com/2015/01/instances-of-non-capturing-lambdas.html
總結(jié)
以上是生活随笔為你收集整理的非捕获Lambda的实例的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美元降息意味着什么?
- 下一篇: AEM中的单元测试(大声思考)