在 Java 中使用 Lambda 表达式的技巧
wangwen625
7年前
<p>在本文中,我们将展示一些在 Java 8 中不太为人所了解的 Lambda 表达式技巧及其使用限制。本文的主要的受众是 Java 开发人员,研究人员以及工具库的编写人员。 这里我们只会使用没有 com.sun 或其他内部类的公共 Java API,如此代码就可以在不同的 JVM 实现之间进行移植。</p> <h2>快速介绍</h2> <p>Lambda 表达式作为在 Java 8 中实现匿名方法的一种途径而被引入,可以在某些场景中作为匿名类的替代方案。 在字节码的层面上来看,Lambda 表达式被替换成了 <a href="/misc/goto?guid=4959751271540979412" rel="nofollow,noindex">invokedynamic</a> 指令。这样的指令曾被用来创建功能接口的实现。 而单个方法则是利用 Lambda 里面所定义的代码将调用委托给实际方法。</p> <p>例如,我们手头有如下代码:</p> <pre> <code class="language-java">void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); }</code></pre> <p>这段代码被 Java 编译器翻译过来就成了下面这样:</p> <pre> <code class="language-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); }</code></pre> <p>invokedynamic 指令可以用 Java 代码粗略的表示成下面这样:</p> <pre> <code class="language-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); }</code></pre> <p>正如你所看见的, <a href="/misc/goto?guid=4959751271627595121" rel="nofollow,noindex">LambdaMetafactory</a> 被用来生成一个调用站点,用目标方法句柄来表示一个工厂方法。这个工厂方法使用了 invokeExact 来返回功能接口的实现。如果 Lambda 封装了变量,则 invokeExact 会接收这些变量拿来作为实参。</p> <p>在 Oracle 的 JRE 8 中,metafactory 会利用 <a href="/misc/goto?guid=4958197083673280437" rel="nofollow,noindex">ObjectWeb Asm</a> 来动态地生成 Java 类,其实现了一个功能接口。 如果 Lambda 表达式封装了外部变量,生成的类里面就会有额外的域被添加进来。这种方法类似于 Java 语言中的匿名类 —— 但是有如下区别:</p> <ul> <li> <p>匿名类是在编译时由 Java 编译器生成的。</p> </li> <li> <p>Lambda 实现的类则是由 JVM 在运行时生成。</p> </li> </ul> <p>metafactory 的如何实现要看是什么 JVM 供应商和版本</p> <p>当然,invokedynamic 指令并不是专门给 Java 中的 lambda 表达式来使用的。引入该指令主要是为了可以在 JVM 之上运行的动态语言。Java 所提供的 <a href="/misc/goto?guid=4959554615990309520" rel="nofollow,noindex">Nashorn JavaScript 引擎</a> 开箱即用,就大大地利用了该指令。</p> <p>在本文的后续内容中,我们将重点介绍 LambdaMetafactory 类及其功能。本文的下一节将假设你已经完全了解了 metafactory 方法如何工作以及 <a href="/misc/goto?guid=4959751271770995747" rel="nofollow,noindex">MethodHandle</a> 是什么。</p> <h2>Lambdas 小技巧</h2> <p>在本节中,我们将介绍如何使用 lambdas 动态构建日常任务。</p> <h2>检查异常和 Lambdas</h2> <p>我们都知道,Java 提供的所有 <a href="/misc/goto?guid=4959671533197482795" rel="nofollow,noindex">函数接口</a> 不支持检查异常。检查与未检查异常在 Java 中打着持久战。</p> <p>如果你想使用与 Java Streams 结合使用的 lambdas 内的检查异常的代码呢? 例如,我们需要将字符串列表转换成 URL 列表,如下所示:</p> <pre> <code class="language-java">Arrays.asList("http://localhost/", "https://github.com") .stream() .map(URL::new) .collect(Collectors.toList())</code></pre> <p><a href="/misc/goto?guid=4959751271892935139" rel="nofollow,noindex">URL(String)</a> 已经在 throws 地方声明了一个检查的异常,因此它不能直接用作 <a href="/misc/goto?guid=4959751271973568061" rel="nofollow,noindex">Function</a> 的方法引用。</p> <p>你说“是的,这里可以使用这样的技巧”:</p> <pre> <code class="language-java">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());</code></pre> <p>这是一个很挫的做法。原因如下:</p> <ul> <li> <p>使用 try-catch 块</p> </li> <li> <p>重新抛出异常</p> </li> <li> <p>Java 中类型擦除的使用不足</p> </li> </ul> <p>这个问题被使用以下方式可以更“合法”的方式解决:</p> <ul> <li> <p>检查的异常仅由 Java 编程语言的编译器识别</p> </li> <li> <p>throws 部分只是方法的元数据,在 JVM 级别没有语义含义</p> </li> <li> <p>检查和未检查的异常在字节码和 JVM 级别是不可区分的</p> </li> </ul> <p>解决的办法是只把 Callable.call 的调用封装在不带 throws 部分的方法之中:</p> <pre> <code class="language-java">static <V> V callUnchecked(Callable<V> callable){ return callable.call(); }</code></pre> <p>这段代码不会被 Java 编译器编译通过,因为方法 Callable.call 在其 throws 部分有受检异常。但是我们可以使用动态构造的 lambda 表达式擦除这个部分。</p> <p>首先,我们要声明一个函数式接口,没有 throws 部分但能够委派调用给 Callable.call:</p> <pre> <code class="language-java">@FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//signature of method INVOKE <V> V invoke(final Callable<V> callable); }</code></pre> <p>第二步是使用 LambdaMetafactory 创建这个接口的实现,以及委派 SilentInvoker.invoke 的方法调用给方法 Callable.call。如前所述,在字节码的级别上 throws 部分被忽略,因此,方法 SilentInvoker.invoke 能够调用方法 Callable.call 而无需声明受检异常:</p> <pre> <code class="language-java">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();</code></pre> <p>第三,写一个实用方法,调用 Callable.call 而不声明受检异常:</p> <pre> <code class="language-java">public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ { return SILENT_INVOKER.invoke(callable); }</code></pre> <p>现在,我们可以毫无顾忌地重写我们的流,使用异常检查:</p> <pre> <code class="language-java">Arrays.asList("http://localhost/", "https://dzone.com") .stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList());</code></pre> <p>此代码将成功编译,因为 callUnchecked 没有被声明为需要检查异常。此外,使用 <a href="/misc/goto?guid=4959751272057042614" rel="nofollow,noindex">单态内联缓存</a> 时可以内联式调用此方法,因为在 JVM 中只有一个实现 SilentInvoker 接口的类。</p> <p>如果实现的 Callable.call 在运行时抛出一些异常,只要它们被捕捉到就没什么问题。</p> <pre> <code class="language-java">try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); }</code></pre> <p>尽管有这样的方法来实现功能,但还是推荐下面的用法:</p> <p>只有当调用代码保证不存在异常时,才能隐藏已检查的异常,才能调用相应的代码。</p> <p>下面的例子演示了这种方法:</p> <pre> <code class="language-java">callUnchecked(() -> new URL("https://dzone.com")); //this URL is always valid and the constructor never throws MalformedURLException</code></pre> <p>这个方法是这个工具的完整实现,在 <a href="/misc/goto?guid=4959751272140313876" rel="nofollow,noindex">这里</a> 它作为开源项目SNAMP的一部分。</p> <p> </p> <p>来自:https://www.oschina.net/translate/hacking-lambda-expressions-in-java</p> <p> </p>