Java8 Lambda 表達式與 Checked Exception

ShoMaddox 7年前發布 | 13K 次閱讀 Java8 Lambda Java開發

當我們在使用 Java 8 的 Lambda 表達式時,表達式內容需要拋出異常,也許還會想當然的讓當前方法再往外拋來解決編譯問題,如下面的代碼

讓 main() 方法拋出 Exception 還是不解決決編譯錯誤,仍然提示 "Unhandled exception: java.io.FileNotFoundException"。

因為我們可能保持著慣性思維,忽略了 Lambda 本身就是一個功能性接口方法的實現,所以把上面的代碼還原為匿名類的方式

public void foo() {
    Stream.of("a", "b").forEach(new Consumer<String>() {
        @Override
        public void accept(String s) {
            new FileInputStream(s).close();
        }
});

那么對于上面那種情況應該如何處理呢?

就地處決或轉換為 unchecked 異常

現在我們就不會讓 foo() 方法去拋出一個異常來捕獲 new FileInputStream(s).close() 這一行可能出現的異常,一定是會讓 accept() 方法來向外層拋異常,正是因為 Consumer 定義的 accept() 方法定義不拋異常,所以若是用 IntelliJ IDEA 的話, 它會提示我們把會產生異常的那行 catch 起來,像下面那樣

try {
    new FileInputStream(s).close();
} catch (IOException e) {
    e.printStackTrace();
    //這是我加的,可以在這里拋出一個 unchecked 異常,如
    throw new RuntimeException("file not found");
}

這種情況似乎只能這樣把 checked exception 轉換為 unchecked exception 了。對于上面的改動,想要 catch 那個 RuntimeException 的話也沒問題

try {
    foo();
} catch (Exception ex) {
    System.out.println(ex.getMessage());  //輸出 file not found
}

使用聲明了異常的 SAM,可在外層方法繼續拋出

如果不想要捕獲異常再轉換為 unchecked exception 的話,那就不能用 Java 8 內置的 Consumer 接口了,需要有一個聲明拋出 Exception 的   accept() 方法的 Consumer . 比如下面的定義的 MyConsumer , 它的 accept() 方法拋出異常,代碼如下:

@FunctionalInterface
    interface MyConsumer {
    void accept(String s) throws Exception;
}

public void foo(String f, MyConsumer consumer) throws Exception {
    consumer.accept(f);
}

public void client() throws Exception{
    foo("a", s -> new FileInputStream(s).close());
}

上面的 foo() 和 client() 方法就可以聲明拋出由 new FileInputStream() 產生的異常,而不需要進行異常轉換。

并發操作是 Lambda 的異常處理

在單線程模式下,異常還是容易處理,有異常時在當前線程的異常棧中能查找到, 而對于其他線程中拋出的異常在當前線程中是無法捕獲到的,就像下面的嘗試是不會成功的

try {
    new Thread(() -> {
        throw new RuntimeException("Something wrong");
    }).start();
    //這里怎么延時也沒用
} catch(Exception ex) {
    System.out.println(ex.getMessage()); //上面的異常永遠也不關這里的事
}

在我的測試下控制臺的輸出類似如下

Exception in thread "Thread-0" java.lang.RuntimeException: Something wrong

at cc.unmi.TestLambdaException.lambda$bar$1(TestLambdaException.java:41)

at java.lang.Thread.run(Thread.java:745)

那么 Java 8 的 parallelStream() 會把任務分配到其他線程去執行,是不是也無法在調用者線程上捕獲到 parallelStream().forEach(Consumer) 中拋出的異常呢?不是的,可正常捕獲,因為 Java 8 的 ForkJoinTask 有進行特殊的處理,會在子線程發生異常時把子線程的異常附著到調用者線程上去。我們來運行下面的代碼

public static void main(String[] args) {
    try {
        foo();
    } catch (Exception ex) {
        System.out.println("Caught exception: " + ex.getMessage());
        ex.printStackTrace();
    }
}

private static void foo() {
    Arrays.asList("a", "b").parallelStream().forEach(s -> {
        try {
            new FileInputStream(s).close();
        } catch (IOException e) {
            throw new RuntimeException("file not found");
        }
    });
}

多次運行上面的代碼可以收到兩種情況的輸出

第一種, 只有一層 RuntimeException("file not found"):

第二種,有兩層的 RuntimeException("file not found")

總之,在使用 Java 8 集合框架的 parallelStream() 可以正常的在啟動線程中捕獲到 Lambda 表達式中產生的異常。

由上圖中可看到起關鍵作用的就是 ForkJoinTask 的 invoke() ,   reportException() , 和   getThrowableException() 方法,可以大概看下相關的 java.util.concurrent.ForkJoinTask 代碼片斷:

    public final V invoke() {
        int s;
        if ((s = doInvoke() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }

    private void reportException(int s) {
        ...........
        if (s == EXCEPTIONAL)
            rethrow(getThrowableException());
    }

    private Throwable getThrowableException() {
        //..... 異常表中查找異常
        Throwable ex;
        .....
        if (e.thrower != Thread.currentThread().getId()) {
            //..... 如果異常的拋出者不是當前線程,把構建出一個異常實例關聯實際異常
        }
        return ex; //這樣就可以把原本在子線程中產生的異常拋到當前線程上來
    }

這可以作為我們在實際的多線程應用中異常處理的一個參考。

 

來自:http://unmi.cc/java8-lambda-and-checked-exception/

 

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