|
在本文中,我們將展示一些在 Java 8 中不太為人所了解的 Lambda 表達(dá)式技巧及其使用限制。本文的主要的受眾是 Java 開(kāi)發(fā)人員,研究人員以及工具庫(kù)的編寫(xiě)人員。 這里我們只會(huì)使用沒(méi)有 com.sun 或其他內(nèi)部類(lèi)的公共 Java API,如此代碼就可以在不同的 JVM 實(shí)現(xiàn)之間進(jìn)行移植。 快速介紹 Lambda 表達(dá)式作為在 Java 8 中實(shí)現(xiàn)匿名方法的一種途徑而被引入,可以在某些場(chǎng)景中作為匿名類(lèi)的替代方案。 在字節(jié)碼的層面上來(lái)看,Lambda 表達(dá)式被替換成了 invokedynamic 指令。這樣的指令曾被用來(lái)創(chuàng)建功能接口的實(shí)現(xiàn)。 而單個(gè)方法則是利用 Lambda 里面所定義的代碼將調(diào)用委托給實(shí)際方法。 例如,我們手頭有如下代碼:
invokedynamic 指令可以用 Java 代碼粗略的表示成下面這樣: 正如你所看見(jiàn)的,LambdaMetafactory 被用來(lái)生成一個(gè)調(diào)用站點(diǎn),用目標(biāo)方法句柄來(lái)表示一個(gè)工廠方法。這個(gè)工廠方法使用了 invokeExact 來(lái)返回功能接口的實(shí)現(xiàn)。如果 Lambda 封裝了變量,則 invokeExact 會(huì)接收這些變量拿來(lái)作為實(shí)參。 在 Oracle 的 JRE 8 中,metafactory 會(huì)利用 ObjectWeb Asm 來(lái)動(dòng)態(tài)地生成 Java 類(lèi),其實(shí)現(xiàn)了一個(gè)功能接口。 如果 Lambda 表達(dá)式封裝了外部變量,生成的類(lèi)里面就會(huì)有額外的域被添加進(jìn)來(lái)。這種方法類(lèi)似于 Java 語(yǔ)言中的匿名類(lèi) —— 但是有如下區(qū)別:
metafactory 的如何實(shí)現(xiàn)要看是什么 JVM 供應(yīng)商和版本 當(dāng)然,invokedynamic 指令并不是專(zhuān)門(mén)給 Java 中的 lambda 表達(dá)式來(lái)使用的。引入該指令主要是為了可以在 JVM 之上運(yùn)行的動(dòng)態(tài)語(yǔ)言。Java 所提供的 Nashorn JavaScript 引擎開(kāi)箱即用,就大大地利用了該指令。 在本文的后續(xù)內(nèi)容中,我們將重點(diǎn)介紹 LambdaMetafactory 類(lèi)及其功能。本文的下一節(jié)將假設(shè)你已經(jīng)完全了解了 metafactory 方法如何工作以及 MethodHandle 是什么。 Lambdas 小技巧 在本節(jié)中,我們將介紹如何使用 lambdas 動(dòng)態(tài)構(gòu)建日常任務(wù)。 檢查異常和 Lambdas 我們都知道,Java 提供的所有函數(shù)接口不支持檢查異常。檢查與未檢查異常在 Java 中打著持久戰(zhàn)。 如果你想使用與 Java Streams 結(jié)合使用的 lambdas 內(nèi)的檢查異常的代碼呢? 例如,我們需要將字符串列表轉(zhuǎn)換成 URL 列表,如下所示:
你說(shuō)“是的,這里可以使用這樣的技巧”: 這是一個(gè)很挫的做法。原因如下:
這個(gè)問(wèn)題被使用以下方式可以更“合法”的方式解決:
解決的辦法是只把 Callable.call 的調(diào)用封裝在不帶 throws 部分的方法之中: 這段代碼不會(huì)被 Java 編譯器編譯通過(guò),因?yàn)榉椒?nbsp;Callable.call 在其 throws 部分有受檢異常。但是我們可以使用動(dòng)態(tài)構(gòu)造的 lambda 表達(dá)式擦除這個(gè)部分。 首先,我們要聲明一個(gè)函數(shù)式接口,沒(méi)有 throws 部分但能夠委派調(diào)用給 Callable.call: 第二步是使用 LambdaMetafactory 創(chuàng)建這個(gè)接口的實(shí)現(xiàn),以及委派 SilentInvoker.invoke 的方法調(diào)用給方法 Callable.call。如前所述,在字節(jié)碼的級(jí)別上 throws 部分被忽略,因此,方法 SilentInvoker.invoke 能夠調(diào)用方法 Callable.call 而無(wú)需聲明受檢異常: 第三,寫(xiě)一個(gè)實(shí)用方法,調(diào)用 Callable.call 而不聲明受檢異常:
現(xiàn)在,我們可以毫無(wú)顧忌地重寫(xiě)我們的流,使用異常檢查:
此代碼將成功編譯,因?yàn)?callUnchecked 沒(méi)有被聲明為需要檢查異常。此外,使用單態(tài)內(nèi)聯(lián)緩存時(shí)可以?xún)?nèi)聯(lián)式調(diào)用此方法,因?yàn)樵?JVM 中只有一個(gè)實(shí)現(xiàn) SilentInvoker 接口的類(lèi)。 如果實(shí)現(xiàn)的 Callable.call 在運(yùn)行時(shí)拋出一些異常,只要它們被捕捉到就沒(méi)什么問(wèn)題。
盡管有這樣的方法來(lái)實(shí)現(xiàn)功能,但還是推薦下面的用法: 只有當(dāng)調(diào)用代碼保證不存在異常時(shí),才能隱藏已檢查的異常,才能調(diào)用相應(yīng)的代碼。 下面的例子演示了這種方法:
這個(gè)方法是這個(gè)工具的完整實(shí)現(xiàn),在這里它作為開(kāi)源項(xiàng)目SNAMP的一部分。 使用 Getter 和 Setter ![]() 這一節(jié)對(duì)不同數(shù)據(jù)格式(如 JSON,Thrift 等等)的序列化/反序列化的編寫(xiě)工具有用。此外,如果你的代碼嚴(yán)重依賴(lài)了為 JavaBean 的 getter 和 setter 準(zhǔn)備的 Java 反射,那么它及其有用。 getter 是在 JavaBean 中的一個(gè)使用 getXXX 命名的無(wú)參且非 Void 返回類(lèi)型的方法、setter 是在 JavaBea n中的一個(gè)使用 setXXX 命名的有一個(gè)單獨(dú)參數(shù)并返回 void 類(lèi)型的方法。這兩個(gè)符號(hào)可以被表示為函數(shù)的接口:
現(xiàn)在,我們創(chuàng)建兩個(gè)方法,這兩個(gè)方法可以把任何 getter 或 setter 轉(zhuǎn)換為這些函數(shù)的接口。無(wú)論這兩個(gè)函數(shù)接口是不是通用。只要類(lèi)型擦除掉之后,真實(shí)的類(lèi)型都等于一個(gè)對(duì)象。自動(dòng)的構(gòu)造一個(gè)返回類(lèi)型和可以被 LambdaMetafactory 識(shí)別的參數(shù)。此外,uava's Cache 幫助為相同的 getter 或 setter 緩存 lambdas 。 首先,必須為 getter 和 setter 聲明一個(gè)緩存。從 Reflection API 上看,Method 表示一個(gè)真實(shí)的 getter 或 setter,并且做為一個(gè) Key 被使用。在緩存中的值代表對(duì)于特定的 getter 或 setter 的動(dòng)態(tài)構(gòu)造函數(shù)接口。
其次,創(chuàng)建工廠方法通過(guò)從方法句柄中指向一個(gè) getter 或 setter 來(lái)創(chuàng)建一個(gè)函數(shù)接口的實(shí)例:
通過(guò) samMethodType 和 instantiatedMethodType(分別為方法 metafactory 的第三個(gè)和第五個(gè)參數(shù))之間的區(qū)別,可以實(shí)現(xiàn)類(lèi)型擦除后的函數(shù)接口中基于對(duì)象的參數(shù)和實(shí)際參數(shù)類(lèi)型之間的自動(dòng)轉(zhuǎn)換以及 getter 或 setter 中的返回類(lèi)型。實(shí)例化的方法類(lèi)型是提供專(zhuān)門(mén)的 lambda 的方法實(shí)現(xiàn)。 然后,為這些工廠具有緩存的支持,創(chuàng)建一個(gè)門(mén)面:
作為使用 Java 反射 API 的 Method 實(shí)例,獲取的方法信息可以輕松地轉(zhuǎn)換為 MethodHandle??紤]到實(shí)例方法總是有隱藏的第一個(gè)參數(shù)用于將其傳遞給方法。靜態(tài)方法沒(méi)有這些隱藏的參數(shù)。例如,Integer.intValue()方法具有 int intValue 的實(shí)際簽名(Integer this)。這個(gè)技巧用于實(shí)現(xiàn) getter 和 setter 的功能包裝器。 現(xiàn)在是時(shí)候測(cè)試代碼了:
這種使用緩存的 getter 和 setter 的方法可以在諸如 Jackson 這樣的序列化和反序列化庫(kù)中高效的使用,這些庫(kù)在序列化/反序列化庫(kù)的過(guò)程中使用 getter 和 setter。 使用 LambdaMetafactory 來(lái)動(dòng)態(tài)生成的實(shí)現(xiàn)調(diào)用函數(shù)接口比通過(guò) Java Reflection API 的調(diào)用快 得多 你可以在這里找到完整的代碼,它是開(kāi)源項(xiàng)目 SNAMP 的一部分。 限制和缺陷 在本節(jié)中,我們將給出在 Java 編譯器和 JVM 中與 lambdas 相關(guān)的一些錯(cuò)誤和限制。 所有這些限制都可以在 OpenJDK 和 Oracle JDK 上重現(xiàn),它們適用于 Windows 和 Linux 的 javac 1.8.0_131。 從方法句柄構(gòu)建 Lambdas如你所知,可以使用 LambdaMetafactory 動(dòng)態(tài)構(gòu)建 lambda。要實(shí)現(xiàn)這一點(diǎn),你應(yīng)該指定一個(gè) MethodHandle,其中包含一個(gè)由函數(shù)接口聲明的單個(gè)方法的實(shí)現(xiàn)。我們來(lái)看看這個(gè)簡(jiǎn)單的例子:
上面代碼等價(jià)于: 但如果我們用一個(gè)可以表示一個(gè)字段獲取方法的方法處理器來(lái)替換指向 getValue 的方法處理器的話,情況會(huì)如何呢:
該代碼應(yīng)該是可以按照預(yù)期來(lái)運(yùn)行的,因?yàn)?findGetter 會(huì)返回一個(gè)指向字段獲取方法、并且具備有效簽名的方法處理器。 但是如果你運(yùn)行了代碼,就會(huì)看到如下異常:
有趣的是,如果我們使用 MethodHandleProxies,字段獲取方法卻可以運(yùn)行得很好:
要注意 MethodHandleProxies 并非動(dòng)態(tài)創(chuàng)建 lambda 表達(dá)式的理想方法,因?yàn)檫@個(gè)類(lèi)只是把 MethodHandle 封裝到一個(gè)代理類(lèi)里面,然后把對(duì) InvocationHandler.invoke 的調(diào)用指派給了 MethodHandle.invokeWithArguments 方法。 這種方法使得 Java 反射機(jī)制運(yùn)行起來(lái)非常的慢。 如前所述,并不是所有的方法句柄都可以在運(yùn)行時(shí)用于構(gòu)建 lambdas。 只有幾種與方法相關(guān)的方法句柄可以用于 lambda 表達(dá)式的動(dòng)態(tài)構(gòu)造 這包括:
其他方法的句柄將會(huì)觸發(fā) LambdaConversionException 異常。 泛型異常 ![]() 這個(gè) bug 與 Java 編譯器以及在 throws 部分聲明泛型異常的能力有關(guān)。下面的示例代碼演示了這種行為:
這段代碼應(yīng)該編譯成功因?yàn)?URL 構(gòu)造器拋出 MalformedURLException。但事實(shí)并非如此。編譯器產(chǎn)生以下錯(cuò)誤消息:
但如果我們用一個(gè)匿名類(lèi)替換 lambda 表達(dá)式,那么代碼就編譯成功了:
結(jié)論很簡(jiǎn)單: 當(dāng)與 lambda 表達(dá)式配合使用時(shí),泛型異常的類(lèi)型推斷不能正確工作。 泛型邊界 ![]() 一個(gè)帶有多個(gè)邊界的泛型可以用 & 號(hào)構(gòu)造:<T extends A & B & C & ... Z>。這種泛型參數(shù)定義很少被使用,但由于其局限性,它對(duì) Java 中的 lambda 表達(dá)式有某些影響:
第二個(gè)局限性使 Java 編譯器在編譯時(shí)和 JVM 在運(yùn)行時(shí)產(chǎn)生不同的行為,當(dāng) Lambda 表達(dá)式的聯(lián)動(dòng)發(fā)生時(shí)。可以使用以下代碼重現(xiàn)此行為: 這段代碼絕對(duì)沒(méi)錯(cuò),而且用 Java 編譯器編譯也會(huì)成功。MutableInteger 這個(gè)類(lèi)可以滿足泛型 T 的多個(gè)類(lèi)型綁定約束:
但是在運(yùn)行的時(shí)候會(huì)拋出異常:
之所以會(huì)這樣是因?yàn)?Java Stream 的管道只捕獲到了一個(gè)原始類(lèi)型,它是一個(gè) Number 類(lèi)。Number 類(lèi)本身并沒(méi)有實(shí)現(xiàn) IntSupplier 接口。 要修復(fù)此問(wèn)題,可以在一個(gè)作為方法引用的單獨(dú)方法中明確定義一個(gè)參數(shù)類(lèi)型:
這個(gè)示例就演示了 Java 編譯器和運(yùn)行時(shí)所進(jìn)行的一次不正確的類(lèi)型推斷。 在 Java 中的編譯時(shí)和運(yùn)行時(shí)處理與 lambdas 結(jié)合的多個(gè)類(lèi)型綁定會(huì)導(dǎo)致不兼容。 |
|
|
來(lái)自: Levy_X > 《JAVAWEB學(xué)習(xí)資料》