在 Java 中使用 Lambda 表达式的技巧

Lambda Java   2017-08-10 09:20:51 发布
您的评价:
     
0.0
收藏     1收藏
文件夹
标签
(多个标签用逗号分隔)

在本文中,我们将展示一些在 Java 8 中不太为人所了解的 Lambda 表达式技巧及其使用限制。本文的主要的受众是 Java 开发人员,研究人员以及工具库的编写人员。 这里我们只会使用没有 com.sun 或其他内部类的公共 Java API,如此代码就可以在不同的 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) { //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 会接收这些变量拿来作为实参。

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

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

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

metafactory 的如何实现要看是什么 JVM 供应商和版本

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

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

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 接口的类。

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

 

来自:https://www.oschina.net/translate/hacking-lambda-expressions-in-java

 

扩展阅读

Python高级编程技巧
Java 8时间和日期API 20例
Android 开发中的日常积累
使用 Lambda 在 Android 中的替代匿名类
Java中不同的并发实现的性能比较

为您推荐

Java Json 序列化与反序列化
嵌入式设备web服务器比较
Struts2自定义过滤器 + 百度富文本控件UEditor + Smb上传图片到独立服务器
注入类型的Android框架Butterknife示例和原理
优化PHP代码的40条建议

更多

Lambda
Java
Java开发
相关文档  — 更多
相关经验  — 更多
相关讨论  — 更多