在本文中,我们将展示一些在 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 动态构建日常任务。
我们都知道,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的一部分。
评论删除后,数据将无法恢复
评论(18)
不过Lambda还有不少问题, 这篇文章 主要是用JSR292 引入的MethodHandle来处理Lambda的一些问题 ; 抽空看看JSR292 应该就可以看明白了
引用来自“izee”的评论
没看出有什么技巧可言,这篇文章应该是想说怎么填Lambda的坑吧Lambda最主要的目的是为了精简代码并提高可读性,但是这篇文章反而让代码变得复杂了,恕在下看不懂
引用来自“简约_bolin”的评论
那个异常检查把我绕晕了。。。如果你感兴趣,推荐学习一下java范型的类型擦除,本例中一个知识点:用类型参数作强制类型转换时,其实会被擦除为类型参数的上边界。
引用来自“公孙二狗”的评论
确信上面的代码 System.out.println("Item = %s", item) 能编译通过?System.out.printf("Item = %s", item)
引用来自“yhh晓龍zhh”的评论
威什么上面的写法都有要用反射写法 ?强行复杂一波。这篇文章只适合一部分人阅读,更多的人只需要lambada的基本用法就可以了,那很容易学,底层的东西是用来解决特殊问题的。