在 Java 中使用 Lambda 表达式的技巧 已翻译 100%

oschina 投递于 2017/08/08 11:13 (共 21 段, 翻译完成于 08-09)
阅读 4509
收藏 122
4
加载中

在本文中,我们将展示一些在 Java 8 中不太为人所了解的 Lambda 表达式技巧及其使用限制。本文的主要的受众是 Java 开发人员,研究人员以及工具库的编写人员。 这里我们只会使用没有 com.sun 或其他内部类的公共 Java API,如此代码就可以在不同的 JVM 实现之间进行移植。

快速介绍

Lambda 表达式作为在 Java 8 中实现匿名方法的一种途径而被引入,可以在某些场景中作为匿名类的替代方案。 在字节码的层面上来看,Lambda 表达式被替换成了 invokedynamic 指令。这样的指令曾被用来创建功能接口的实现。 而单个方法则是利用 Lambda 里面所定义的代码将调用委托给实际方法。

LeoXu
LeoXu
翻译于 2017/08/08 11:23
0

例如,我们手头有如下代码:

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 指令可以用 Java 代码粗略的表示成下面这样:

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 被用来生成一个调用站点,用目标方法句柄来表示一个工厂方法。这个工厂方法使用了 invokeExact 来返回功能接口的实现。如果 Lambda 封装了变量,则 invokeExact 会接收这些变量拿来作为实参。

LeoXu
LeoXu
翻译于 2017/08/08 11:28
0

在 Oracle 的 JRE 8 中,metafactory 会利用 ObjectWeb Asm 来动态地生成 Java 类,其实现了一个功能接口。 如果 Lambda 表达式封装了外部变量,生成的类里面就会有额外的域被添加进来。这种方法类似于 Java 语言中的匿名类 —— 但是有如下区别:

  • 匿名类是在编译时由 Java 编译器生成的。

  • Lambda 实现的类则是由 JVM 在运行时生成。

metafactory 的如何实现要看是什么 JVM 供应商和版本
LeoXu
LeoXu
翻译于 2017/08/08 11:35
0

当然,invokedynamic 指令并不是专门给 Java 中的 lambda 表达式来使用的。引入该指令主要是为了可以在 JVM 之上运行的动态语言。Java 所提供的 Nashorn JavaScript 引擎开箱即用,就大大地利用了该指令。

在本文的后续内容中,我们将重点介绍 LambdaMetafactory 类及其功能。本文的下一节将假设你已经完全了解了 metafactory 方法如何工作以及 MethodHandle 是什么。

LeoXu
LeoXu
翻译于 2017/08/08 11:39
0

Lambdas 小技巧

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

检查异常和 Lambdas

我们都知道,Java 提供的所有函数接口不支持检查异常。检查与未检查异常在 Java 中打着持久战。

如果你想使用与 Java Streams 结合使用的 lambdas 内的检查异常的代码呢? 例如,我们需要将字符串列表转换成 URL 列表,如下所示:

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

URL(String)已经在 throws 地方声明了一个检查的异常,因此它不能直接用作 Function 的方法引用。

亚林瓜子
亚林瓜子
翻译于 2017/08/08 15:13
0

你说“是的,这里可以使用这样的技巧”:

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());

这是一个很挫的做法。原因如下:

  • 使用 try-catch 块

  • 重新抛出异常

  • Java 中类型擦除的使用不足

这个问题被使用以下方式可以更“合法”的方式解决:

  • 检查的异常仅由 Java 编程语言的编译器识别

  • throws 部分只是方法的元数据,在 JVM 级别没有语义含义

  • 检查和未检查的异常在字节码和 JVM 级别是不可区分的

亚林瓜子
亚林瓜子
翻译于 2017/08/08 15:27
0

解决的办法是只把 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);
}

圣洁之子
圣洁之子
翻译于 2017/08/08 15:55
0

第二步是使用 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);
}

圣洁之子
圣洁之子
翻译于 2017/08/08 16:05
0

现在,我们可以毫无顾忌地重写我们的流,使用异常检查:

Arrays.asList("http://localhost/", "https://dzone.com")
.stream()
.map(url -> callUnchecked(() -> new URL(url)))
.collect(Collectors.toList());

此代码将成功编译,因为 callUnchecked 没有被声明为需要检查异常。此外,使用单态内联缓存时可以内联式调用此方法,因为在 JVM 中只有一个实现 SilentInvoker 接口的类。

Tocy
Tocy
翻译于 2017/08/08 13:08
0

如果实现的 Callable.call 在运行时抛出一些异常,只要它们被捕捉到就没什么问题。

try{
    callUnchecked(() -> new URL("Invalid URL"));
} catch (final Exception e){
    System.out.println(e);
}

尽管有这样的方法来实现功能,但还是推荐下面的用法:

只有当调用代码保证不存在异常时,才能隐藏已检查的异常,才能调用相应的代码。

下面的例子演示了这种方法:

callUnchecked(() -> new URL("https://dzone.com")); //this URL is always valid and the constructor never throws MalformedURLException

这个方法是这个工具的完整实现,在这里它作为开源项目SNAMP的一部分。

溪边九节
溪边九节
翻译于 2017/08/08 17:41
0
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(18)

jackerx
jackerx
之前调用方法 可以用反射; JDK7 增加了JSR292 动态方法调用 比反射性能好更灵活; JDK8 加上Lambda 更进一步 ;
不过Lambda还有不少问题, 这篇文章 主要是用JSR292 引入的MethodHandle来处理Lambda的一些问题 ; 抽空看看JSR292 应该就可以看明白了
old_big
old_big

引用来自“izee”的评论

没看出有什么技巧可言,这篇文章应该是想说怎么填Lambda的坑吧
Lambda最主要的目的是为了精简代码并提高可读性,但是这篇文章反而让代码变得复杂了,恕在下看不懂
java有许多坑,其他语言也有坑,初级程序员不需要关注这些坑,也不需要去填,高级程序员会熟悉语言的边界,懂得哪些是精华,那些是陷阱,会用代码技巧解决特殊问题,复杂性控制在小范围内,暴露给初级程序员最简单的接口。
old_big
old_big

引用来自“简约_bolin”的评论

那个异常检查把我绕晕了。。。
这里其实用了一个范型的小技巧去欺骗编译器。
如果你感兴趣,推荐学习一下java范型的类型擦除,本例中一个知识点:用类型参数作强制类型转换时,其实会被擦除为类型参数的上边界。
old_big
old_big

引用来自“公孙二狗”的评论

确信上面的代码 System.out.println("Item = %s", item) 能编译通过?
原文就错了,应该是:
System.out.printf("Item = %s", item)
old_big
old_big

引用来自“yhh晓龍zhh”的评论

威什么上面的写法都有要用反射写法 ?强行复杂一波。
上边有说到,反射调用方法的性能较低。
old_big
old_big
都看懂了,这是java8发布以来读到最好的一篇关于lambda表达式的文章,收获很大, 译者和原著辛苦了!
这篇文章只适合一部分人阅读,更多的人只需要lambada的基本用法就可以了,那很容易学,底层的东西是用来解决特殊问题的。
yhh晓龍zhh
yhh晓龍zhh
威什么上面的写法都有要用反射写法 ?强行复杂一波。
哈斯卡会
哈斯卡会
一脸懵逼
公孙二狗
公孙二狗
确信上面的代码 System.out.println("Item = %s", item) 能编译通过?
kidfruit
kidfruit
这篇文章是翻译的吧?写的这么绕口。java8的lambda设计是我见过的最烂的没有之一,这篇文章说的有些问题其实可以后期迭代改进,但是我觉得java8 lambda的设计从根基开始就跑偏了,一点都不优雅。
返回顶部
顶部