
译者的话:LambdaMetafactory可能是Java 8中被低估的机制之一,我们最近发现了它,但已经对其功能感到赞赏。 CUBA框架7.0版通过避免使用生成Lambda表达式的反射调用来提高性能。 这种机制在我们的框架中的应用之一是通过注释将应用程序事件处理程序绑定,这是一个常见的任务,是Spring的EventListener的类似物。 我们相信LambdaFactory原理的知识在许多Java应用程序中可能会有用,因此我们希望与您共享此翻译。
在本文中,我们将展示一些在Java 8中使用lambda表达式时鲜为人知的技巧以及这些表达式的局限性。 本文的目标读者是高级Java开发人员,研究人员和工具包开发人员。 只有公共Java API才能使用com.sun.*
和其他内部类,因此代码可在不同的JVM实现之间移植。
简短的前言
Lambda表达式作为一种实现匿名方法的方式出现在Java 8中,
在某些情况下,可以替代匿名类。 在字节码级别,lambda表达式被invokedynamic
替换。 该指令用于创建功能接口实现,其唯一方法将调用委派给实际方法,该方法包含在lambda表达式主体中定义的代码。
例如,我们有以下代码:
void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); }
该代码将由Java编译器转换为类似于以下内容的代码:
private static void lambda_forEach(String item) {
invokedynamic
指令可以大致表示为以下Java代码:
private static CallSite cs; void printElements(List<String> strings) { Consumer<String> lambda;
如您所见, LambdaMetafactory
用于创建CallSite ,该CallSite提供了一种工厂方法,该方法返回目标方法的处理程序。 此方法使用invokeExact
返回功能接口的实现。 如果lambda表达式中包含捕获的变量,则invokeExact
这些变量作为实际参数接受。
在Oracle JRE 8中,metafactory使用ObjectWeb Asm动态生成Java类,该Java类创建实现功能接口的类。 如果lambda表达式捕获外部变量,则可以将其他字段添加到创建的类中。 这个看起来像Java匿名类,但是有以下区别:
- Java编译器生成一个匿名类。
- 用于实现lambda表达式的类是由JVM在运行时创建的。
元工厂实现取决于JVM供应商和版本
当然, invokedynamic
不仅用于Java中的lambda表达式。 它主要在JVM环境中运行动态语言时使用。 Java内置的Nashorn JavaScript 引擎大量使用了此指令。
接下来,我们将重点介绍LambdaMetafactory
类及其功能。 下一个
本文的这一部分假定您非常了解元工厂方法的工作方式以及MethodHandle
什么
Lambda表达式技巧
在本节中,我们将展示如何构建用于日常任务的动态lambda。
检查异常和lambda
众所周知,Java中存在的所有功能接口都不支持检查的异常。 与常规例外相比,检查例外的优势是一个长期存在(并且仍然很热门)的争论。
但是,如果您需要在lambda表达式中将带有检查异常的代码与Java Streams结合使用呢? 例如,您需要将字符串列表转换为如下所示的URL列表:
Arrays.asList("http://localhost/", "https://github.com").stream() .map(URL::new) .collect(Collectors.toList())
URL(String)的构造函数中声明了引发异常,因此不能将其直接用作Functiion类中的方法引用。
您会说:“不,如果您在这里使用此技巧,也许会这样:”
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); }
这是一个肮脏的hack。 这就是为什么:
- 使用try-catch块。
- 再次引发异常。
- Java中类型擦除的肮脏用法。
可以使用以下事实的知识以更“合法”的方式解决问题:
- 已检查的异常仅在Java编译器级别被识别。
throws
部分只是在JVM级别上没有语义值的方法的元数据。- 在JVM中,在字节码级别上,已检查异常和普通异常是无法区分的。
解决方案是将Callable.call
方法包装在没有throws
节的方法中:
static <V> V callUnchecked(Callable<V> callable){ return callable.call(); }
由于Callable.call
方法在throws
部分中声明了Callable.call
检查的异常,因此无法编译此代码。 但是我们可以使用动态构造的lambda表达式删除此部分。
首先,我们需要声明一个没有throws
部分的功能接口。
但谁可以将呼叫委托给Callable.call
:
@FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);
第二步是使用LambdaMetafactory
创建此接口的实现,并将SilentInvoker.invoke
方法的Callable.call
委派给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) { return SILENT_INVOKER.invoke(callable); }
现在,您可以重写流,而检查异常不会有任何问题:
Arrays.asList("http://localhost/", "https://dzone.com").stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList());
此代码编译没有问题,因为callUnchecked
不会声明已检查的异常。 此外,可以使用单态内联缓存来内联调用此方法,因为在整个JVM中,只有一个类实现了SilentOnvoker
接口
如果Callable.call
的实现在运行时引发异常,则调用函数将Callable.call
该异常,而不会出现任何问题:
try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); }
尽管可以使用此方法,但您应始终记住以下建议:
仅当您确定被调用的代码不会引发任何异常时,才使用callUnchecked隐藏检查的异常
以下示例显示了此方法的示例:
callUnchecked(() -> new URL("https://dzone.com"));
此方法的完整实现在这里 ,它是SNAMP开源项目的一部分 。
与Getter和Setter合作
对于编写各种数据格式(例如JSON,Thrift等)的序列化/反序列化的人员,本节将非常有用。 而且,如果您的代码严重依赖JavaBeans中的Getter和Setter的反射,那么它可能会非常有用。
在JavaBean中声明的getter是一个名为getXXX
的方法,该方法没有任何参数,并且返回的数据类型不是void
。 在JavaBean中声明的setter是一个名为setXXX
的方法,该方法具有一个参数并返回void
。 这两个符号可以表示为功能接口:
现在,我们将创建两个可以将任何getter或setter转换为这些方法的方法
功能接口。 两个接口都是通用的也没关系。 删除类型后
真正的数据类型将是Object
。 使用LambdaMetafactory
可以自动转换返回类型和参数。 另外, Guava库将帮助为相同的getter和setter缓存lambda表达式。
第一步:为getter和setter创建一个缓存。 Reflection API的Method类代表一个真实的getter或setter,并用作键。
缓存值是用于特定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),
使用samMethodType
和InstantiatedMethodType(分别是metafactory方法的第三个和第五个参数)之间的差异,可以实现功能接口中的Object
类型的参数(在类型擦除之后),实samMethodType
类型和返回值之间的自动类型转换。 创建的方法实例的类型-这是提供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 Reflection API从Method
类的实例获取的方法信息可以轻松转换为MethodHandle
。 请记住,类实例方法始终具有用于将其传递给此方法的隐藏的第一个参数。 静态方法没有这样的参数。 例如, Integer.intValue()
方法的实际签名看起来像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);
这种具有缓存的getter和setter的方法可以有效地用于在序列化和反序列化过程中使用getter和setter的序列化/反序列化库(例如Jackson)。
使用LambdaMetaFactory
调用具有动态生成的实现的功能接口比通过Java Reflection API调用要快得多
完整代码版本可以在SNAMP库中找到。
局限性和错误
在本节中,我们将研究与Java编译器和JVM中的lambda表达式相关的一些错误和限制。 所有这些限制都可以在适用于Windows和Linux的javac
1.8.0_131版的OpenJDK和Oracle JDK中重现。
从方法处理程序创建lambda表达式
如您所知,可以使用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());
但是,如果我们用getter字段表示的处理程序替换指向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),
该代码应该可以正常工作,因为findGetter
返回一个指向getter字段并具有正确签名的处理程序。 但是,如果运行此代码,则会看到以下异常:
java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField
有趣的是,如果使用MethodHandleProxies ,则该字段的getter可以正常工作:
final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj));
应当注意, MethodHandleProxies
不是动态创建lambda表达式的好方法,因为此类仅将MethodHandle
包装在代理类中并将invocationHandler.invoke委托给MethodHandle.invokeWithArguments 。 这种方法使用Java反射并且非常慢。
如前所述,并非所有方法处理程序都可用于在运行时创建lambda表达式。
仅几种类型的方法处理程序可用于动态创建lambda表达式。
它们是:
其他类型的处理程序将LambdaConversionException
错误。
通用异常
此错误与Java编译器以及在throws
部分中引发通用异常的能力有关。 下面的代码示例演示了此行为:
interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("http://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("http://localhost"); } }; urlFactory.call();
由此得出:
与lambda表达式结合使用时,泛型异常的类型推断无法正常工作
参数化类型限制
您可以使用&
构造具有几个类型限制的通用对象: <T extends A & B & C & ... Z>
符号。
这种确定通用参数的方法很少使用,但是由于某些限制,在某种程度上会影响Java中的lambda表达式:
- 除第一个约束外,每个类型约束都必须是一个接口。
- 具有此类泛型的类的纯版本仅考虑列表中的第一个类型约束。
第二个局限性是在发生绑定到lambda表达式时,导致代码在编译时和运行时的行为不同。 可以使用以下代码证明这种差异:
final class MutableInteger extends Number implements IntSupplier, IntConsumer {
该代码是绝对正确的,并且可以成功编译。 MutableInteger
类满足通用类型T的约束:
MutableInteger
继承自Number
。MutableInteger
实现IntSupplier
。
但是代码会在运行时崩溃,并带有异常:
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)
发生这种情况是因为JavaStream管道仅捕获一个纯类型,在我们的例子中,它是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(); }
本示例说明了在编译器和运行时中错误的类型推断。
在编译时和运行时结合使用lambda表达式来处理多个通用参数类型限制是不一致的