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