开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
在 Java 中使用 Lambda 表达式的技巧 - 技术翻译 - 开源中国社区

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

标签: <无>
oschina 推荐于 2周前 (共 21 段, 翻译完成于 08-09) 评论 18
收藏  
119
推荐标签: 待读

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

快速介绍

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

leoxu
 翻译得不错哦!

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

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
 翻译得不错哦!

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

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

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

metafactory 的如何实现要看是什么 JVM 供应商和版本
leoxu
 翻译得不错哦!

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

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

leoxu
 翻译得不错哦!

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 的方法引用。

亚林瓜子
 翻译得不错哦!

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

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 级别是不可区分的

亚林瓜子
 翻译得不错哦!

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

圣洁之子
 翻译得不错哦!

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

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

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

Tocy
 翻译得不错哦!

如果实现的 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的一部分。

无若
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(18)
Ctrl/CMD+Enter

那个异常检查把我绕晕了。。。
没看出有什么技巧可言,这篇文章应该是想说怎么填Lambda的坑吧
Lambda最主要的目的是为了精简代码并提高可读性,但是这篇文章反而让代码变得复杂了,恕在下看不懂
好吧,一定是我java没学好,没看懂
完全没看懂,
成功把我绕晕了
我学了个假java
java设计的好烂,建议大家看 msdn 杂志相关文档,看看原创者是如何一步步设计出来的。这个和语言无关。
知道不只我一个没看懂就放心了
这篇文章是翻译的吧?写的这么绕口。java8的lambda设计是我见过的最烂的没有之一,这篇文章说的有些问题其实可以后期迭代改进,但是我觉得java8 lambda的设计从根基开始就跑偏了,一点都不优雅。
确信上面的代码 System.out.println("Item = %s", item) 能编译通过?
一脸懵逼
威什么上面的写法都有要用反射写法 ?强行复杂一波。
都看懂了,这是java8发布以来读到最好的一篇关于lambda表达式的文章,收获很大, 译者和原著辛苦了!
这篇文章只适合一部分人阅读,更多的人只需要lambada的基本用法就可以了,那很容易学,底层的东西是用来解决特殊问题的。

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

威什么上面的写法都有要用反射写法 ?强行复杂一波。
上边有说到,反射调用方法的性能较低。

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

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

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

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

引用来自“izee”的评论

没看出有什么技巧可言,这篇文章应该是想说怎么填Lambda的坑吧
Lambda最主要的目的是为了精简代码并提高可读性,但是这篇文章反而让代码变得复杂了,恕在下看不懂
java有许多坑,其他语言也有坑,初级程序员不需要关注这些坑,也不需要去填,高级程序员会熟悉语言的边界,懂得哪些是精华,那些是陷阱,会用代码技巧解决特殊问题,复杂性控制在小范围内,暴露给初级程序员最简单的接口。
之前调用方法 可以用反射; JDK7 增加了JSR292 动态方法调用 比反射性能好更灵活; JDK8 加上Lambda 更进一步 ;
不过Lambda还有不少问题, 这篇文章 主要是用JSR292 引入的MethodHandle来处理Lambda的一些问题 ; 抽空看看JSR292 应该就可以看明白了
顶部