原創|行業資訊|編輯:郝浩|2017-08-11 17:52:08.000|閱讀 1090 次
概述:本文將從字節碼(Bytecode)的級別研究Lambda表達式是如何工作的,以及如何將它與getter、setter和其它技巧組合起來的。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
在本文中,我們將介紹Java 8中Lambda表達式的一些鮮為人知的技巧及其局限性,其主要受眾包括中高級Java開發人員、研究人員和工具編寫者。在這里我們將只使用公共Java API而不使用com.sun和其它的內部類,因此代碼可以在不同的JVM中實現。
Lambda表達式在Java 8中被引入,作為一種實現匿名函數的方法,在某些情況下,可作為匿名類的替代方案。在字節碼(Bytecode)的級別中,Lambda表達式用invokedynamic指令替代,該指令能夠簡化JVM上動態類型語言的編譯器和運行時系統的實現。其delegates類能夠調用Lambda主體內所定義的代碼的實例。
例如,我們有以下代碼:
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指令可以將其粗略地表達為以下代碼:
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用于生成某個目標函數(匿名類)在工廠模式下的調用點(call site)。而工廠模式會返回這個函數接口在使用invokeExact的實現結果。如果Lambda附加了變量,那么invokeExact將會把這些變量作為實際參數。
在Oracle JRE 8中,metafactory會使用,通過實現函數接口的方式,動態生成一個Java類。如果Lambda表達式包含外部變量,則可以在生成類中添加附加字段。這種方法類似于Java語言中的匿名類,但有以下的不同點:
注意:metafactory的實現依賴于JVM供應商和版本
invokedynamic指令并不只用于Java中的Lambda表達式,該指令的引入主要是為了JVM之上動態語言的運行。Nashorn,Java開箱即用的下一代JavaScript引擎中大量地使用了這個指令。
在本文的后面部分,我們將重點討論LambdaMetafactory類及其功能。本文的下一節是基于假設你完全理解了metafactory方法的工作原理和方法。
在本節中我們將介紹如何在日常任務中使用Lambda的動態構建。
并不是Java提供的所有函數接口都支持受檢查異常。是否支持受檢查異常在Java世界中是一場古老的圣戰。
如果為了結合使用Java Stream,你需要lambda中含有受檢查異常的代碼,那該怎么做?比如,我們需要將字符串列表轉換成這樣的url列表:
Arrays.asList("//localhost/", "//github.com") .stream() .map(URL::new) .collect(Collectors.toList())
在throws中已聲明了受檢查異常,因此,它不能在中直接作為函數引用。
你可能會說:“這沒問題啊,我可以這么干。”
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());
這個做法并不高明,原因如下:
上述行為所想要解決的問題我們可以更“規范”的作如下表達:
解決方法是在函數中包裹Callable.call的調用,而不引入throws的部分:
static <V> V callUnchecked(Callable<V> callable){ return callable.call(); }
這段代碼不會被Java編譯器所編譯,因為Callable.call的throws部分包含受檢查異常。但是我們可以使用動態構建的lambda表達式來刪除這個部分。
首先,我們應當聲明一個沒有throws部分但能夠委托調用Callable.call的函數接口:
@FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//signature of method INVOKE <V> V invoke(final Callable<V> callable); }
第二步是使用LambdaMetafactory創建這個接口的實現,并委托SilentInvoker.invoke調用Callable.call。如前所述,在字節碼級別,throws部分被忽略了,因此,SilentInvoker.invoke可以在不聲明受檢查異常的情況下調用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();
第三步編寫在不需要聲明受檢查異常的情況下調用Callable.call的函數。
public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ { return SILENT_INVOKER.invoke(callable); }
現在,我們可以毫無問題的使用檢查異常重寫stream。
Arrays.asList("//localhost/", "//dzone.com") .stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList());
這段代碼會被成功編譯,因為callUnchecked沒有聲明受檢查異常。此外,由于JVM中只有一個類來實現接口SilentInvoker,因此調用此方法可能會使用單態內聯緩存。
如果Callable.call在運行時拋出了一些異常,它將會通過調用來進行捕捉,而不會出現任何問題:
try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); }
盡管有這樣的方法來實現功能,但我還是強烈推薦以下的用法:
只有當調用代碼保證了無異常產生的情況下才使用callUnchecked隱藏受檢查異常。
下面的示例演示了這種方法:
callUnchecked(() -> new URL("//dzone.com")); //this URL is always valid and the constructor never throws MalformedURLException
這個方法的可在開源項目中找到。
這一節對于編寫JSON、Thrift等不同格式的序列化/反序列化的程序員很有幫助。另外,如果你的代碼嚴重依賴于用于JavaBean的getter和setter的Java反射,那么它將讓你收益良多。
JavaBean中聲明的getter,命名為getXXX,是無參數和非void返回類型的函數,JavaBean中聲明的setter,命名為setXXX,是帶有單個參數和返回類型為void的函數。它們可以表示為這樣的函數接口:
現在我們創建兩個可將任意getter或setter轉換成這些函數接口的方法。這兩個函數接口是否為泛型并不重要。在類型消除之后,實際的類型等于對象。自動選擇返回類型和參數可以由LambdaMetafactory完成。此外,有助于緩存有相同getter或setter的lambda。
首先,有必要為getter和setter聲明一個緩存,來自Reflection API的代表了當前getter或setter,并作為一個key使用。緩存中的值表示特定getter或setter的動態構造函數接口。
private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build();
其次,創建工廠方法,通過從方法句柄中指向getter或setter來創建函數接口的實例:
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); } }
通過對samMethodType和instantiatedMethodType(分別對應metafactory的第三個和第五個參數)之間的區分,可以實現類型擦除后的函數接口中基于對象的參數和實際參數類型之間的自動轉換并以getter或setter作為返回類型。實例化方法類型是提供lambda實現的特殊方法。
然后,在緩存的支持下,為這些工廠創建一個外觀:
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 實例,獲取的方法信息可以輕松地轉換為 MethodHandle。考慮到實例方法總是有隱藏的第一個參數用于將其傳遞給方法。靜態方法沒有這些隱藏的參數。例如,方法具有 int intValue 的實際簽名(Integer this)。這個技巧用于實現 getter 和 setter 的功能包裝器。
現在是時候測試代碼了:
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動態生成的實現調用函數接口比通過Java Reflection API的調用要。
你可以在開源項目中找到。
在本節中,我們將給出在 Java 編譯器和 JVM 中與 lambdas 相關的一些錯誤和限制。 所有這些限制都可以在 OpenJDK 和 Oracle JDK 上重現,它們適用于 Windows 和 Linux 的 javac 1.8.0_131。
如你所知,可以使用 LambdaMetafactory 動態構建 lambda。要實現這一點,你應該指定一個 MethodHandle,其中包含一個由函數接口聲明的單個方法的實現。我們來看看這個簡單的例子:
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());
上面的代碼等價于:
final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final Supplier<String> elementGetter = () -> obj.getValue(); System.out.println(elementGetter.get());
但如果我們用一個可以表示一個字段獲取方法的方法處理器來替換指向 getValue 的方法處理器的話,情況會如何呢:
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));
該代碼應該是可以按照預期來運行的,因為 findGetter 會返回一個指向字段獲取方法、并且具備有效簽名的方法處理器。 但是如果你運行了代碼,就會看到如下異常:
java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField
有趣的是,如果我們使用 ,字段獲取方法卻可以運行得很好:
final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj));
要注意 MethodHandleProxies 并非動態創建 lambda 表達式的理想方法,因為這個類只是把 MethodHandle 封裝到一個代理類里面,然后把對的調用指派給了 方法。 這種方法使得 Java 反射機制運行起來非常的慢。
如前所述,并不是所有的方法句柄都可以在運行時用于構建 lambdas。
只有幾種與方法相關的方法句柄可以用于 lambda 表達式的動態構造
這包括:
其他方法的句柄將會觸發 LambdaConversionException 異常。
這個 bug 與 Java 編譯器以及在 throws 部分聲明泛型異常的能力有關。下面的示例代碼演示了這種行為:
interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("//localhost"); urlFactory.call();
這段代碼應該編譯成功因為 URL 構造器拋出 MalformedURLException。但事實并非如此。編譯器產生以下錯誤消息:
Error:(46, 73) java: call() in <.anonymous Test$CODEgt; cannot implement call() in ExtendedCallable overridden method does not throw java.lang.Exception
但如果我們用一個匿名類替換 lambda 表達式,那么代碼就編譯成功了:
final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("//localhost"); } }; urlFactory.call();
結論很簡單:
當與lambda表達式配合使用時,泛型異常的類型推斷不能正確工作。
一個帶有多個邊界的泛型可以用 & 號構造:<T extends A & B & C & ... Z>。這種泛型參數定義很少被使用,但由于其局限性,它對 Java 中的 lambda 表達式有某些影響:
第二個局限性使 Java 編譯器在編譯時和 JVM 在運行時產生不同的行為,當 Lambda 表達式的聯動發生時。可以使用以下代碼重現此行為:
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);
這段代碼絕對沒錯,而且用 Java 編譯器編譯也會成功。MutableInteger 這個類可以滿足泛型 T 的多個類型綁定約束:
但是在運行的時候會拋出異常:
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)
之所以會這樣是因為 Java Stream 的管道只捕獲到了一個原始類型,它是一個 Number 類。Number 類本身并沒有實現 IntSupplier 接口。 要修復此問題,可以在一個作為方法引用的單獨方法中明確定義一個參數類型:
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(); }
這個示例就演示了 Java 編譯器和運行時所進行的一次不正確的類型推斷。
在 Java 中的編譯時和運行時處理與 lambdas 結合的多個類型綁定會導致不兼容。
本文翻譯自
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn