Java8:當 Lambda 遇上受檢異常

Java8:當 Lambda 遇上受檢異常

Java8:當 Lambda 遇上受檢異常

前言

我今天高高興興,想寫個簡單的統計一個項目下有多少行代碼的小程序,於是咔咔的寫下:

long count = Files.walk(Paths.get("D:/Test")) // 獲得項目目錄下的所有目錄及文件
.filter(file -> !Files.isDirectory(file)) // 篩選出文件
.filter(file -> file.toString().endsWith(".java")) // 篩選出 java 文件
.flatMap(file -> Files.lines(file)) // 按行獲得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 過濾掉空行
.count();
System.out.println("代碼行數:" + count);

題外話:

Files.walk(Path) 在 JDK1.8 時添加,深度優先遍歷一個 Path (目錄),返回這個目錄下所有的Path(目錄和文件),通過 Stream<path> 返回;/<path>

Files.lines(Path) 也是在 JDK1.8 時添加,功能是返回指定Path(文件)中所有的行,通過 Stream<string> 返回/<string>

然後,編譯不過 —— 因為 Files.lines(Path) 會拋出 IOException,如果要編譯通過,得這樣寫:

long count = Files.walk(Paths.get("D:/Test")) // 獲得項目目錄下的所有文件
.filter(file -> !Files.isDirectory(file)) // 篩選出文件
.filter(file -> file.toString().endsWith(".java")) // 篩選出 java 文件

.flatMap(file -> {
try {
return Files.lines(file);
} catch (IOException ex) {
ex.printStackTrace(System.err);
return Stream.empty(); // 拋出異常時返回一個空的 Stream
}
}) // 按行獲得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 過濾掉空行
.count();
System.out.println("代碼行數:" + count);

我的天,這個時候我強迫症就犯了——因為這樣的 Lambda 不是 one-liner expression,不夠簡潔,也不直觀。如果 Stream的流式操作中多幾個需要拋出受檢異常的情況,那代碼真是太難看了,所以為了 one-liner expression 的 Lambda,我們需要解決的辦法。

解決方案

解決方法一

通過新建一個方法( :) 無奈但是純潔的微笑)

public static void main(String[] args) throws Exception {
long count = Files.walk(Paths.get("D:/Test")) // 獲得項目目錄下的所有文件
.filter(file -> !Files.isDirectory(file)) // 篩選出文件
.filter(file -> file.toString().endsWith(".java")) // 篩選出 java 文件
.flatMap(file -> getLines(file)) // 按行獲得文件中的文本
.filter(line -> !line.trim().isEmpty()) // 過濾掉空行
.count();
System.out.println("代碼行數:" + count);

}
private static Stream<string> getLines(Path file) {
try {
return Files.lines(file);
} catch (IOException ex) {
ex.printStackTrace(System.err);
return Stream.empty();
}
}
/<string>

這種解決方法下,我們需要處理受檢異常 —— 即在程序拋出異常的時候,我們需要告訴程序怎麼去做(getLines 方法中拋出異常時我們輸出了異常,並返回一個空的 Stream)

解決方法二

將會拋出異常的函數進行包裝,使其不拋出受檢異常

如果一個 FunctionInterface 的方法會拋出受檢異常(比如 Exception),那麼該 FunctionInterface 便可以作為會拋出受檢異常的 Lambda 的目標類型。

我們定義如下一個受檢的 FunctionInterface:

@FunctionalInterface
interface CheckedFunction {
R apply(T t) throws Throwable;
}

那麼該 FunctionalInterface 便可以作為類似於file -> File.lines(file) 這類會拋出受檢異常的 Lambda 的目標類型,此時 Lambda 中並不需要捕獲異常(因為目標類型的 apply 方法已經將異常拋出了)—— 之所以原來的 Lambda 需要捕獲異常,就是因為在流式操作 flatMap 中使用的 java.util.function 包下的 Function

沒有拋出異常:

java.util.function.Function

那我們如何使用 CheckedFunction 到流式操作的 Lambda 中呢?

首先我們定義一個 Attempt 接口,它的 apply 靜態方法提供將 CheckedFunction 包裝為 Function 的功能:

public interface Attempt {
static Function apply(CheckedFunction function) {
Objects.requireNonNull(function);
return t -> {
try {
return function.apply(t);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
}

然後在原先的代碼中,我們使用 Attempt.apply 方法來對會拋出受檢異常的 Lambda 進行包裝:

long count = Files.walk(Paths.get("D:/Test")) // 獲得項目目錄下的所有文件
.filter(file -> !Files.isDirectory(file)) // 篩選出文件
.filter(file -> file.toString().endsWith(".java")) // 篩選出 java 文件
.flatMap(Attempt.apply(file -> Files.lines(file))) // 將 會拋出受檢異常的 Lambda 包裝為 拋出非受檢異常的 Lambda
.filter(line -> !line.trim().isEmpty()) // 過濾掉空行
.count();

System.out.println("代碼行數:" + count);

此時,我們便可以選擇是否去捕獲異常(RuntimeException)。這種解決方法下,我們一般不關心拋出異常的情況 —— 比如自己寫的小例子,拋出了異常程序就該終止;或者你知道這個 Lambda 確實 100% 不會拋出異常。

不過我更傾向於拋出異常時,我們來指定處理的方式:

static  Function apply(CheckedFunction function, Function<throwable> handler) {
Objects.requireNonNull(function);
Objects.requireNonNull(handler);
return t -> {
try {
return function.apply(t);
} catch (Throwable e) {
return handler.apply(e);
}
};
}
/<throwable>

比如我們前面的例子,如果 file -> Files.lines(file) 拋出異常了,說明在訪問 file 類的時候出了問題,我們可以就假設這個文件的行數為 0 ,那麼默認值就是個空的 Stream<string>(當然你也可以選擇順手記錄一下異常):/<string>

long count = Files.walk(Paths.get("D:/Test")) // 獲得項目目錄下的所有文件
.filter(file -> !Files.isDirectory(file)) // 篩選出文件

.filter(file -> file.toString().endsWith(".java")) // 篩選出 java 文件
.flatMap(TryTo.apply(file -> Files.lines(file), ex -> Stream.empty()))
.filter(line -> !line.trim().isEmpty()) // 過濾掉空行
.count();
System.out.println("代碼行數:" + count);

使用 CheckedFunction這種方式更為通用 —— 類似的,我們可以包裝 CheckedConsumer 為 java.util.function.Consumer,包裝 CheckedSupplier 為 java.util.function.Suppiler,CheckedBiFunction 為 java.util.function.BiFunction 等:

public interface Attempt {
......
/**
* 包裝受檢的 Consumer
*/
static Consumer accept(CheckedConsumer consumer) {
Objects.requireNonNull(consumer);
return t -> {
try {
consumer.accept(t);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
/**
* 包裝受檢的 Consumer,並自定義異常處理
*/
static Consumer accept(CheckedConsumer consumer, Consumer<throwable> handler) {
Objects.requireNonNull(consumer);
Objects.requireNonNull(handler);
return t -> {
try {
consumer.accept(t);
} catch (Throwable e) {
handler.accept(e);
}
};
}

}
/<throwable>

本文 Attempt 接口的代碼:公眾號回覆:20191015Attempt

就我個人觀點而言,我真的不喜歡 Java 中的受檢(Checked)異常,我認為所有的異常都應該是非受檢(Unchecked)的 —— 因為一段代碼如果會產生異常,我們自然會去解決這個問題直到其不拋出異常或者捕獲這個異常並做對應處理 —— 強制性的要求編碼人員捕獲異常,帶來的更多的是編碼上的不方便和代碼可讀性的降低(因為冗餘)。不過既然受檢異常已經是 Java 中的客觀存在的事物,所謂“道高一尺,魔高一丈” —— 總是會有辦法來應對。

推薦

大廠筆試內容集合(內有詳細解析) 持續更新中….

ProcessOn是一個在線作圖工具的聚合平臺~

文末

歡迎關注Coder編程

公眾號,主要分享數據結構與算法、Java相關知識體系、框架知識及原理、Spring全家桶、微服務項目實戰、DevOps實踐之路、每日一篇互聯網大廠面試或筆試題以及PMP項目管理知識等。更多精彩內容正在路上~

Java8:當 Lambda 遇上受檢異常


分享到:


相關文章: