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

wangwen625 7年前發布 | 27K 次閱讀 Lambda Java Java開發

在本文中,我們將展示一些在 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

 

 本文由用戶 wangwen625 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!