原創(chuàng)|行業(yè)資訊|編輯:郝浩|2017-08-11 17:52:08.000|閱讀 1090 次
概述:本文將從字節(jié)碼(Bytecode)的級(jí)別研究Lambda表達(dá)式是如何工作的,以及如何將它與getter、setter和其它技巧組合起來的。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
在本文中,我們將介紹Java 8中Lambda表達(dá)式的一些鮮為人知的技巧及其局限性,其主要受眾包括中高級(jí)Java開發(fā)人員、研究人員和工具編寫者。在這里我們將只使用公共Java API而不使用com.sun和其它的內(nèi)部類,因此代碼可以在不同的JVM中實(shí)現(xiàn)。
Lambda表達(dá)式在Java 8中被引入,作為一種實(shí)現(xiàn)匿名函數(shù)的方法,在某些情況下,可作為匿名類的替代方案。在字節(jié)碼(Bytecode)的級(jí)別中,Lambda表達(dá)式用invokedynamic指令替代,該指令能夠簡(jiǎn)化JVM上動(dòng)態(tài)類型語言的編譯器和運(yùn)行時(shí)系統(tǒng)的實(shí)現(xiàn)。其delegates類能夠調(diào)用Lambda主體內(nèi)所定義的代碼的實(shí)例。
例如,我們有以下代碼:
void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); }
這段代碼由Java編譯器編譯后成為這樣:
private static void lambda_forEach(String item) { //generated by Java compiler System.out.println("Item = %s", item); } private static CallSite bootstrapLambda(Lookup lookup, String name, MethodType type) { // //lookup = provided by VM //name = "lambda_forEach", provided by VM //type = String -> void MethodHandle lambdaImplementation = lookup.findStatic(lookup.lookupClass(), name, type); return LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(Consumer.class), //signature of lambda factory MethodType.methodType(void.class, Object.class), //signature of method Consumer.accept after type erasure lambdaImplementation, //reference to method with lambda body type); } void printElements(List < String > strings) { Consumer < String > lambda = invokedynamic# bootstrapLambda, #lambda_forEach strings.forEach(lambda); }
invokedynamic指令可以將其粗略地表達(dá)為以下代碼:
private static CallSite cs; void printElements(List < String > strings) { Consumer < String > lambda; //begin invokedynamic if (cs == null) cs = bootstrapLambda(MethodHandles.lookup(), "lambda_forEach", MethodType.methodType(void.class, String.class)); lambda = (Consumer < String > ) cs.getTarget().invokeExact(); //end invokedynamic strings.forEach(lambda); }
正如你所看到的,LambdaMetafactory用于生成某個(gè)目標(biāo)函數(shù)(匿名類)在工廠模式下的調(diào)用點(diǎn)(call site)。而工廠模式會(huì)返回這個(gè)函數(shù)接口在使用invokeExact的實(shí)現(xiàn)結(jié)果。如果Lambda附加了變量,那么invokeExact將會(huì)把這些變量作為實(shí)際參數(shù)。
在Oracle JRE 8中,metafactory會(huì)使用,通過實(shí)現(xiàn)函數(shù)接口的方式,動(dòng)態(tài)生成一個(gè)Java類。如果Lambda表達(dá)式包含外部變量,則可以在生成類中添加附加字段。這種方法類似于Java語言中的匿名類,但有以下的不同點(diǎn):
注意:metafactory的實(shí)現(xiàn)依賴于JVM供應(yīng)商和版本
invokedynamic指令并不只用于Java中的Lambda表達(dá)式,該指令的引入主要是為了JVM之上動(dòng)態(tài)語言的運(yùn)行。Nashorn,Java開箱即用的下一代JavaScript引擎中大量地使用了這個(gè)指令。
在本文的后面部分,我們將重點(diǎn)討論LambdaMetafactory類及其功能。本文的下一節(jié)是基于假設(shè)你完全理解了metafactory方法的工作原理和方法。
在本節(jié)中我們將介紹如何在日常任務(wù)中使用Lambda的動(dòng)態(tài)構(gòu)建。
并不是Java提供的所有函數(shù)接口都支持受檢查異常。是否支持受檢查異常在Java世界中是一場(chǎng)古老的圣戰(zhàn)。
如果為了結(jié)合使用Java Stream,你需要lambda中含有受檢查異常的代碼,那該怎么做?比如,我們需要將字符串列表轉(zhuǎn)換成這樣的url列表:
Arrays.asList("//localhost/", "//github.com") .stream() .map(URL::new) .collect(Collectors.toList())
在throws中已聲明了受檢查異常,因此,它不能在中直接作為函數(shù)引用。
你可能會(huì)說:“這沒問題啊,我可以這么干。”
public static <T> T uncheckCall(Callable<T> callable) { try { return callable.call(); } catch (Exception e) { return sneakyThrow(e); } } private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E { throw (E)t; } public static <T> T sneakyThrow(Throwable e) { return Util.<RuntimeException, T>sneakyThrow0(e); } // Usage sample //return s.filter(a -> uncheckCall(a::isActive)) // .map(Account::getNumber) // .collect(toSet());
這個(gè)做法并不高明,原因如下:
上述行為所想要解決的問題我們可以更“規(guī)范”的作如下表達(dá):
解決方法是在函數(shù)中包裹Callable.call的調(diào)用,而不引入throws的部分:
static <V> V callUnchecked(Callable<V> callable){ return callable.call(); }
這段代碼不會(huì)被Java編譯器所編譯,因?yàn)镃allable.call的throws部分包含受檢查異常。但是我們可以使用動(dòng)態(tài)構(gòu)建的lambda表達(dá)式來刪除這個(gè)部分。
首先,我們應(yīng)當(dāng)聲明一個(gè)沒有throws部分但能夠委托調(diào)用Callable.call的函數(shù)接口:
@FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//signature of method INVOKE <V> V invoke(final Callable<V> callable); }
第二步是使用LambdaMetafactory創(chuàng)建這個(gè)接口的實(shí)現(xiàn),并委托SilentInvoker.invoke調(diào)用Callable.call。如前所述,在字節(jié)碼級(jí)別,throws部分被忽略了,因此,SilentInvoker.invoke可以在不聲明受檢查異常的情況下調(diào)用Callable.call。
private static final SilentInvoker SILENT_INVOKER; final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "invoke", MethodType.methodType(SilentInvoker.class), SilentInvoker.SIGNATURE, lookup.findVirtual(Callable.class, "call", MethodType.methodType(Object.class)), SilentInvoker.SIGNATURE); SILENT_INVOKER = (SilentInvoker) site.getTarget().invokeExact();
第三步編寫在不需要聲明受檢查異常的情況下調(diào)用Callable.call的函數(shù)。
public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ { return SILENT_INVOKER.invoke(callable); }
現(xiàn)在,我們可以毫無問題的使用檢查異常重寫stream。
Arrays.asList("//localhost/", "//dzone.com") .stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList());
這段代碼會(huì)被成功編譯,因?yàn)閏allUnchecked沒有聲明受檢查異常。此外,由于JVM中只有一個(gè)類來實(shí)現(xiàn)接口SilentInvoker,因此調(diào)用此方法可能會(huì)使用單態(tài)內(nèi)聯(lián)緩存。
如果Callable.call在運(yùn)行時(shí)拋出了一些異常,它將會(huì)通過調(diào)用來進(jìn)行捕捉,而不會(huì)出現(xiàn)任何問題:
try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); }
盡管有這樣的方法來實(shí)現(xiàn)功能,但我還是強(qiáng)烈推薦以下的用法:
只有當(dāng)調(diào)用代碼保證了無異常產(chǎn)生的情況下才使用callUnchecked隱藏受檢查異常。
下面的示例演示了這種方法:
callUnchecked(() -> new URL("//dzone.com")); //this URL is always valid and the constructor never throws MalformedURLException
這個(gè)方法的可在開源項(xiàng)目中找到。
這一節(jié)對(duì)于編寫JSON、Thrift等不同格式的序列化/反序列化的程序員很有幫助。另外,如果你的代碼嚴(yán)重依賴于用于JavaBean的getter和setter的Java反射,那么它將讓你收益良多。
JavaBean中聲明的getter,命名為getXXX,是無參數(shù)和非void返回類型的函數(shù),JavaBean中聲明的setter,命名為setXXX,是帶有單個(gè)參數(shù)和返回類型為void的函數(shù)。它們可以表示為這樣的函數(shù)接口:
現(xiàn)在我們創(chuàng)建兩個(gè)可將任意getter或setter轉(zhuǎn)換成這些函數(shù)接口的方法。這兩個(gè)函數(shù)接口是否為泛型并不重要。在類型消除之后,實(shí)際的類型等于對(duì)象。自動(dòng)選擇返回類型和參數(shù)可以由LambdaMetafactory完成。此外,有助于緩存有相同getter或setter的lambda。
首先,有必要為getter和setter聲明一個(gè)緩存,來自Reflection API的代表了當(dāng)前getter或setter,并作為一個(gè)key使用。緩存中的值表示特定getter或setter的動(dòng)態(tài)構(gòu)造函數(shù)接口。
private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build();
其次,創(chuàng)建工廠方法,通過從方法句柄中指向getter或setter來創(chuàng)建函數(shù)接口的實(shí)例:
private static Function createGetter(final MethodHandles.Lookup lookup, final MethodHandle getter) throws Exception{ final CallSite site = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), //signature of method Function.apply after type erasure getter, getter.type()); //actual signature of getter try { return (Function) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } } private static BiConsumer createSetter(final MethodHandles.Lookup lookup, final MethodHandle setter) throws Exception { final CallSite site = LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(BiConsumer.class), MethodType.methodType(void.class, Object.class, Object.class), //signature of method BiConsumer.accept after type erasure setter, setter.type()); //actual signature of setter try { return (BiConsumer) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } }
通過對(duì)samMethodType和instantiatedMethodType(分別對(duì)應(yīng)metafactory的第三個(gè)和第五個(gè)參數(shù))之間的區(qū)分,可以實(shí)現(xiàn)類型擦除后的函數(shù)接口中基于對(duì)象的參數(shù)和實(shí)際參數(shù)類型之間的自動(dòng)轉(zhuǎn)換并以getter或setter作為返回類型。實(shí)例化方法類型是提供lambda實(shí)現(xiàn)的特殊方法。
然后,在緩存的支持下,為這些工廠創(chuàng)建一個(gè)外觀:
public static Function reflectGetter(final MethodHandles.Lookup lookup, final Method getter) throws ReflectiveOperationException { try { return GETTERS.get(getter, () -> createGetter(lookup, lookup.unreflect(getter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } public static BiConsumer reflectSetter(final MethodHandles.Lookup lookup, final Method setter) throws ReflectiveOperationException { try { return SETTERS.get(setter, () -> createSetter(lookup, lookup.unreflect(setter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } }
作為使用 Java 反射 API 的 Method 實(shí)例,獲取的方法信息可以輕松地轉(zhuǎn)換為 MethodHandle。考慮到實(shí)例方法總是有隱藏的第一個(gè)參數(shù)用于將其傳遞給方法。靜態(tài)方法沒有這些隱藏的參數(shù)。例如,方法具有 int intValue 的實(shí)際簽名(Integer this)。這個(gè)技巧用于實(shí)現(xiàn) getter 和 setter 的功能包裝器。
現(xiàn)在是時(shí)候測(cè)試代碼了:
final Date d = new Date(); final BiConsumer<Date, Long> timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class)); timeSetter.accept(d, 42L); //the same as d.setTime(42L); final Function<Date, Long> timeGetter = reflectGetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("getTime")); System.out.println(timeGetter.apply(d)); //the same as d.getTime() //output is 42
這種緩存getter和setter的方法可以有效地用于序列化和反序列化期間,使用getter和setter的序列化/反序列化庫(如Jackson)。
使用LambdaMetafactory動(dòng)態(tài)生成的實(shí)現(xiàn)調(diào)用函數(shù)接口比通過Java Reflection API的調(diào)用要。
你可以在開源項(xiàng)目中找到。
在本節(jié)中,我們將給出在 Java 編譯器和 JVM 中與 lambdas 相關(guān)的一些錯(cuò)誤和限制。 所有這些限制都可以在 OpenJDK 和 Oracle JDK 上重現(xiàn),它們適用于 Windows 和 Linux 的 javac 1.8.0_131。
如你所知,可以使用 LambdaMetafactory 動(dòng)態(tài)構(gòu)建 lambda。要實(shí)現(xiàn)這一點(diǎn),你應(yīng)該指定一個(gè) MethodHandle,其中包含一個(gè)由函數(shù)接口聲明的單個(gè)方法的實(shí)現(xiàn)。我們來看看這個(gè)簡(jiǎn)單的例子:
final class TestClass { String value = ""; public String getValue() { return value; } public void setValue(final String value) { this.value = value; } } final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findVirtual(TestClass.class, "getValue", MethodType.methodType(String.class)), MethodType.methodType(String.class)); final Supplier<String> getter = (Supplier<String>) site.getTarget().invokeExact(obj); System.out.println(getter.get());
上面的代碼等價(jià)于:
final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final Supplier<String> elementGetter = () -> obj.getValue(); System.out.println(elementGetter.get());
但如果我們用一個(gè)可以表示一個(gè)字段獲取方法的方法處理器來替換指向 getValue 的方法處理器的話,情況會(huì)如何呢:
final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findGetter(TestClass.class, "value", String.class), //field getter instead of method handle to getValue MethodType.methodType(String.class));
該代碼應(yīng)該是可以按照預(yù)期來運(yùn)行的,因?yàn)?findGetter 會(huì)返回一個(gè)指向字段獲取方法、并且具備有效簽名的方法處理器。 但是如果你運(yùn)行了代碼,就會(huì)看到如下異常:
java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField
有趣的是,如果我們使用 ,字段獲取方法卻可以運(yùn)行得很好:
final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj));
要注意 MethodHandleProxies 并非動(dòng)態(tài)創(chuàng)建 lambda 表達(dá)式的理想方法,因?yàn)檫@個(gè)類只是把 MethodHandle 封裝到一個(gè)代理類里面,然后把對(duì)的調(diào)用指派給了 方法。 這種方法使得 Java 反射機(jī)制運(yùn)行起來非常的慢。
如前所述,并不是所有的方法句柄都可以在運(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)。下面的示例代碼演示了這種行為:
interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("//localhost"); urlFactory.call();
這段代碼應(yīng)該編譯成功因?yàn)?URL 構(gòu)造器拋出 MalformedURLException。但事實(shí)并非如此。編譯器產(chǎn)生以下錯(cuò)誤消息:
Error:(46, 73) java: call() in <.anonymous Test$CODEgt; cannot implement call() in ExtendedCallable overridden method does not throw java.lang.Exception
但如果我們用一個(gè)匿名類替換 lambda 表達(dá)式,那么代碼就編譯成功了:
final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("//localhost"); } }; urlFactory.call();
結(jié)論很簡(jiǎn)單:
當(dāng)與lambda表達(dá)式配合使用時(shí),泛型異常的類型推斷不能正確工作。
一個(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)此行為:
final class MutableInteger extends Number implements IntSupplier, IntConsumer { //mutable container of int value private int value; public MutableInteger(final int v) { value = v; } @Override public int intValue() { return value; } @Override public long longValue() { return value; } @Override public float floatValue() { return value; } @Override public double doubleValue() { return value; } @Override public int getAsInt() { return intValue(); } @Override public void accept(final int value) { this.value = value; } } static < T extends Number & IntSupplier > OptionalInt findMinValue(final Collection < T > values) { return values.stream().mapToInt(IntSupplier::getAsInt).min(); } final List < MutableInteger > values = Arrays.asList(new MutableInteger(10), new MutableInteger(20)); final int mv = findMinValue(values).orElse(Integer.MIN_VALUE); System.out.println(mv);
這段代碼絕對(duì)沒錯(cuò),而且用 Java 編譯器編譯也會(huì)成功。MutableInteger 這個(gè)類可以滿足泛型 T 的多個(gè)類型綁定約束:
但是在運(yùn)行的時(shí)候會(huì)拋出異常:
java.lang.BootstrapMethodError: call site initialization exception at java.lang.invoke.CallSite.makeSite(CallSite.java:341) at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307) at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297) at Test.minValue(Test.java:77) Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Number; not a subtype of implementation type interface java.util.function.IntSupplier at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233) at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
之所以會(huì)這樣是因?yàn)?Java Stream 的管道只捕獲到了一個(gè)原始類型,它是一個(gè) Number 類。Number 類本身并沒有實(shí)現(xiàn) IntSupplier 接口。 要修復(fù)此問題,可以在一個(gè)作為方法引用的單獨(dú)方法中明確定義一個(gè)參數(shù)類型:
private static int getInt(final IntSupplier i){ return i.getAsInt(); } private static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection<T> values){ return values.stream().mapToInt(UtilsTest::getInt).min(); }
這個(gè)示例就演示了 Java 編譯器和運(yùn)行時(shí)所進(jìn)行的一次不正確的類型推斷。
在 Java 中的編譯時(shí)和運(yùn)行時(shí)處理與 lambdas 結(jié)合的多個(gè)類型綁定會(huì)導(dǎo)致不兼容。
本文翻譯自
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@fc6vip.cn