Java8 Lambda 表達式與 Checked Exception
當我們在使用 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/