深度剖析Java中的Lambda表达式

行业资讯 | 作者:回忆和感动 | 2017-08-11 17:52:08| 阅读 0 有用 (0) 评论 (0) 收藏


概述:本文将从字节码(Bytecode)的级别研究Lambda表达式是如何工作的,以及如何将它与getter、setter和其它技巧组合起来的。

在本文中,我们将介绍Java 8中Lambda表达式的一些鲜为人知的技巧及其局限性,其主要受众包括中高级Java开发人员、研究人员和工具编写者。在这里我们将只使用公共Java API而不使用com.sun和其它的内部类,因此代码可以在不同的JVM中实现。

快速介绍


Lambda表达式在Java 8中被引入,作为一种实现匿名函数的方法,在某些情况下,可作为匿名类的替代方案。在字节码(Bytecode)的级别中,Lambda表达式用invokedynamic指令替代,该指令能够简化JVM上动态类型语言的编译器和运行时系统的实现。其delegates类能够调用Lambda主体内所定义的代码的实例。

例如,我们有以下代码:

void printElements(List 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会使用ObjectWeb Asm,通过实现函数接口的方式,动态生成一个Java类。如果Lambda表达式包含外部变量,则可以在生成类中添加附加字段。这种方法类似于Java语言中的匿名类,但有以下的不同点:

  • 匿名类是由Java编译器在编译时生成的。
  • 而Lambda实现的类是由JVM在运行时生成的。

注意:metafactory的实现依赖于JVM供应商和版本

invokedynamic指令并不只用于Java中的Lambda表达式,该指令的引入主要是为了JVM之上动态语言的运行。Nashorn,Java开箱即用的下一代JavaScript引擎中大量地使用了这个指令。

在本文的后面部分,我们将重点讨论LambdaMetafactory类及其功能。本文的下一节是基于假设你完全理解了metafactory方法的工作原理和方法。

关于Lambda的技巧


在本节中我们将介绍如何在日常任务中使用Lambda的动态构建。

Lambda与受检查异常(Checked Exception

并不是Java提供的所有函数接口都支持受检查异常。是否支持受检查异常在Java世界中是一场古老的圣战。

如果为了结合使用Java Stream,你需要lambda中含有受检查异常的代码,那该怎么做?比如,我们需要将字符串列表转换成这样的url列表:

Arrays.asList("http://localhost/", "https://github.com")
.stream()
.map(URL::new)
.collect(Collectors.toList())

URL(字符串)在throws中已声明了受检查异常,因此,它不能在Function中直接作为函数引用。

你可能会说:“这没问题啊,我可以这么干。”

public static  T uncheckCall(Callable callable) {
  try { return callable.call(); }
  catch (Exception e) { return sneakyThrow(e); }
}
private static  T sneakyThrow0(Throwable t) throws E { throw (E)t; }
public static  T sneakyThrow(Throwable e) {
  return Util.sneakyThrow0(e);
}
// Usage sample
//return s.filter(a -> uncheckCall(a::isActive))
//        .map(Account::getNumber)
//        .collect(toSet());

这个做法并不高明,原因如下:

  • 使用了try-catch语句。
  • 重新抛出了异常。
  • 使用了Java的类型擦除。

上述行为所想要解决的问题我们可以更“规范”的作如下表达:

  • 受检查异常只能由Java语言的编译器来识别。
  • 在JVM级别上,throws的异常只是无语义函数的元数据。
  • 在字节码和JVM级别,受检查异常和非受检查异常不易区分。

解决方法是在函数中包裹Callable.call的调用,而不引入throws的部分:

static  V callUnchecked(Callable 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 invoke(final Callable 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 callUnchecked(final Callable callable) /*no throws*/ {
    return SILENT_INVOKER.invoke(callable);
}

现在,我们可以毫无问题的使用检查异常重写stream。

Arrays.asList("http://localhost/", "https://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("https://dzone.com")); //this URL is always valid and the constructor never throws MalformedURLException

这个方法的完整实现可在开源项目SNAMP中找到。

与Getters和Setters的协同工作

这一节对于编写JSON、Thrift等不同格式的序列化/反序列化的程序员很有帮助。另外,如果你的代码严重依赖于用于JavaBean的getter和setter的Java反射,那么它将让你收益良多。

JavaBean中声明的getter,命名为getXXX,是无参数和非void返回类型的函数,JavaBean中声明的setter,命名为setXXX,是带有单个参数和返回类型为void的函数。它们可以表示为这样的函数接口:

  • getter可以表示为一个函数参数由this引用的Function
  • setter可以表示为一个第一参数由this引用,第二参数为传递给setter的BiConsumer

现在我们创建两个可将任意getter或setter转换成这些函数接口的方法。这两个函数接口是否为泛型并不重要。在类型消除之后,实际的类型等于对象。自动选择返回类型和参数可以由LambdaMetafactory完成。此外,Guava's Cache有助于缓存有相同getter或setter的lambda。

首先,有必要为getter和setter声明一个缓存,来自Reflection API的Method代表了当前getter或setter,并作为一个key使用。缓存中的值表示特定getter或setter的动态构造函数接口。

private static final Cache GETTERS = CacheBuilder.newBuilder().weakValues().build();
private static final Cache 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。考虑到实例方法总是有隐藏的第一个参数用于将其传递给方法。静态方法没有这些隐藏的参数。例如,Integer.intValue()方法具有 int intValue 的实际签名(Integer this)。这个技巧用于实现 getter 和 setter 的功能包装器。

现在是时候测试代码了:

final Date d = new Date();
final BiConsumer timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class));
timeSetter.accept(d, 42L); //the same as d.setTime(42L);
final Function 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的调用要快得多

你可以在开源项目SNAMP中找到完整的代码

限制和缺陷

在本节中,我们将给出在 Java 编译器和 JVM 中与 lambdas 相关的一些错误和限制。 所有这些限制都可以在 OpenJDK 和 Oracle JDK 上重现,它们适用于 Windows 和 Linux 的 javac 1.8.0_131。

从方法句柄构建 Lambdas

如你所知,可以使用 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 getter = (Supplier) site.getTarget().invokeExact(obj);
System.out.println(getter.get());

上面的代码等价于:

final TestClass obj = new TestClass();
obj.setValue("Hello, world!");
final Supplier 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

有趣的是,如果我们使用 MethodHandleProxies,字段获取方法却可以运行得很好:

final Supplier getter = MethodHandleProxies
                                       .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class)
                                       .bindTo(obj));

要注意 MethodHandleProxies 并非动态创建 lambda 表达式的理想方法,因为这个类只是把 MethodHandle 封装到一个代理类里面,然后把对 InvocationHandler.invoke 的调用指派给了 MethodHandle.invokeWithArguments 方法。 这种方法使得 Java 反射机制运行起来非常的慢。

如前所述,并不是所有的方法句柄都可以在运行时用于构建 lambdas。

只有几种与方法相关的方法句柄可以用于 lambda 表达式的动态构造

这包括:

  • REF_invokeInterface: 对于接口方法可通过 Lookup.findVirtual 来构建
  • REF_invokeVirtual: 对于由类提供的虚方法可以通过 Lookup.findVirtual 来构建
  • REF_invokeStatic: 对于静态方法可通过 Lookup.findStatic 构建
  • REF_newInvokeSpecial: 对于构造函数可通过 Lookup.findConstructor 构建
  • REF_invokeSpecial: 对于私有方法和由类提供的早绑定的虚方法可通过 Lookup.findSpecial 构建

其他方法的句柄将会触发 LambdaConversionException 异常。

泛型异常

这个 bug 与 Java 编译器以及在 throws 部分声明泛型异常的能力有关。下面的示例代码演示了这种行为:

interface ExtendedCallable extends Callable{
        @Override
        V call() throws E;
}
final ExtendedCallable 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 urlFactory = new ExtendedCallable() {
            @Override
            public URL call() throws MalformedURLException {
                return new URL("http://localhost");
            }
        };
urlFactory.call();

结论很简单:

当与lambda表达式配合使用时,泛型异常的类型推断不能正确工作。

泛型边界

一个带有多个边界的泛型可以用 & 号构造:。这种泛型参数定义很少被使用,但由于其局限性,它对 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 的多个类型绑定约束:

  • 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)

之所以会这样是因为 Java Stream 的管道只捕获到了一个原始类型,它是一个 Number 类。Number 类本身并没有实现 IntSupplier 接口。 要修复此问题,可以在一个作为方法引用的单独方法中明确定义一个参数类型:

private static int getInt(final IntSupplier i){
    return i.getAsInt();
}
private static  OptionalInt findMinValue(final Collection values){
    return values.stream().mapToInt(UtilsTest::getInt).min();
}

这个示例就演示了 Java 编译器和运行时所进行的一次不正确的类型推断。

在 Java 中的编译时和运行时处理与 lambdas 结合的多个类型绑定会导致不兼容。

 

本文翻译自dzone.com


慧都控件|提供软件技术整体解决方案

云集全球三千余款优秀控件、软件产品,提供行业领先的咨询、培训与开发服务
企业QQ:800018081|电话:023-66090381

用户评论: 您的宝贵经验,能为更多人带来帮助,登录后才能评论。
评论加载中...



    联系我们


    官方微信
    官方微博

    慧都旗下网站

    友情链接

    慧都科技有限公司 版权所有 Copyright 2003-2016 渝ICP备12000582号 | 京公网安备 11010102002019号
    100%正版软件