Lambda表达式与方法引用的实现细节对比

7

鉴于这个情况:

class MyClass {
    static class A {
        public boolean property() {
            return Math.random() < 0.5;
        }
    }

    static List<A> filterLambda(List<A> list) {
        return list.stream().filter(a -> a.property()).collect(Collectors.toList());
    }

    static List<A> filterMethodCall(List<A> list) {
        return list.stream().filter(A::property).collect(Collectors.toList());
    }
}
  • 每种方法在编译器所做的事情有何不同?
  • 如果有的话,内存使用或运行时间是否有差异?(即使很小,这个问题只是学术上的)

注:我知道这个问题类似于这个问题,但我认为它没有得到正确回答。


1
“编译器的工作原理”可能超出了此处回答的范围。规范提供了一些理论背景信息,但不清楚您想深入了解实际编译器实现的程度。我能确定的是,生成的字节码对于两种方法都是相同的。 - Marco13
3个回答

8
这是从Brett Oken链接的Brian Goetz的文档中提取的内容:
当编译器遇到一个lambda表达式时,它首先将lambda主体降级(解糖)为一个方法,该方法的参数列表和返回类型与lambda表达式匹配,可能还有一些额外的参数(用于从词法作用域中捕获的值,如果有的话)。在 lambda 表达式被捕获的点上,它会生成一个 invokedynamic 调用站点,当调用时,返回将 lambda 转换为的函数接口的实例。这个调用站点被称为给定 lambda 的 lambda 工厂。lambda 工厂的动态参数是从词法作用域中捕获的值。lambda 工厂的引导方法是 Java 语言运行库中的标准化方法,称为 lambda metafactory。静态引导参数捕获了编译时已知的关于 lambda 的信息(将要转换为的函数接口,解糖后的 lambda 主体的方法句柄,有关 SAM 类型是否可序列化的信息等)。
方法引用与 lambda 表达式处理方式相同,只是大多数方法引用不需要解糖为新方法;我们可以简单地加载引用方法的常量方法句柄,并将其传递给元工厂。

以下是从同一文档中提取的示例:

例如,考虑捕获字段minSize的lambda:

list.filter(e -> e.getSize() < minSize )

我们将其转化为实例方法,并将接收者作为第一个捕获的参数传递:
list.forEach(INDY((MH(metaFactory), MH(invokeVirtual Predicate.apply),
                    MH(invokeVirtual B.lambda$1))( this ))));

private boolean lambda$1(Element e) {
    return e.getSize() < minSize; }

list.filter(String::isEmpty)

被翻译为:

list.filter(indy(MH(metaFactory), MH(invokeVirtual Predicate.apply),
             MH(invokeVirtual String.isEmpty))()))

3

lambda表达式和方法引用有区别的一个案例是在作为Supplier接口的一部分使用时。假设我们有以下静态方法:

public static <T, E> T catchException(Supplier<T> resolver) {
    try {
        T result = resolver.get();
        return  result ;
    } catch (Exception e) {
        System.out.println("Exception");
        return null;
    }
}

当我们使用以下的lambda表达式调用方法时,代码可以正常工作,因为lambda表达式作为Supplier的一部分传递,并且仅在调用get()方法并捕获异常时执行。
List<String> underlyers=null;
System.out.println(catchException(()->underlyers.size()));

当我们使用方法引用调用该方法时,如下所示,由于引用在传递给供应商之前被执行,因此在调用方法之前会出现NullPointerExecption。在这种情况下,控制不会到达get()方法。

List<String> underlyers=null;
System.out.println(catchException(underlyers::size));

0

这里是关于方法引用的Java语言规范。

这里是关于Lambda表达式的规范。

从规则的角度来看,Lambda表达式要复杂得多。

然而,在这两种情况下,结果都是一个invokedynamic调用。

Brian Goetz写了一篇文档,介绍了这个工作原理。


我不是在问应该使用哪一个,但感谢提供链接。 - hithwen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接