Java8:當 Lambda 趕上受檢異常

Java8:當 Lambda 趕上受檢異常

前言

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

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> 返回; Files.lines(Path) 也是在 JDK1.8 時添加,功能是返回指定Path(文件)中全部的行,經過 Stream<String> 返回git

而後,編譯不過 —— 由於 Files.lines(Path) 會拋出 IOException,若是要編譯經過,得這樣寫:github

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 expressionLambda,咱們須要解決的辦法。面試

解決方案

解決方法一

經過新建一個方法( :) 無奈可是純潔的微笑)算法

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();
    }
}

複製代碼

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

解決方法二

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

若是一個 FunctionInterface 的方法會拋出受檢異常(好比 Exception),那麼該 FunctionInterface 即可以做爲會拋出受檢異常的 Lambda 的目標類型。小程序

咱們定義以下一個受檢的 FunctionInterfacebash

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

複製代碼

那麼該 FunctionalInterface 即可以做爲相似於file -> File.lines(file) 這類會拋出受檢異常的 Lambda 的目標類型,此時 Lambda 中並不須要捕獲異常(由於目標類型的 apply 方法已經將異常拋出了)—— 之因此原來的 Lambda 須要捕獲異常,就是由於在流式操做 flatMap 中使用的 java.util.function 包下的 Function<T, R> 沒有拋出異常:微信

java.util.function.Function
複製代碼

那咱們如何使用 CheckedFunction 到流式操做的 Lambda 中呢? 首先咱們定義一個 Attempt 接口,它的 apply 靜態方法提供將 CheckedFunction 包裝爲 Function 的功能:

public interface Attempt {

   static <T, R> Function<T, R> apply(CheckedFunction<T, R> 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 <T, R> Function<T, R> apply(CheckedFunction<T, R> function, Function<Throwable, R> handler) {
    Objects.requireNonNull(function);
    Objects.requireNonNull(handler);

    return t -> {
        try {
            return function.apply(t);
        } catch (Throwable e) {
            return handler.apply(e);
        }
    };
}
複製代碼

好比咱們前面的例子,若是 file -> Files.lines(file) 拋出異常了,說明在訪問 file 類的時候出了問題,咱們能夠就假設這個文件的行數爲 0 ,那麼默認值就是個空的 Stream<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這種方式更爲通用 —— 相似的,咱們能夠包裝 CheckedConsumerjava.util.function.Consumer,包裝 CheckedSupplierjava.util.function.SuppilerCheckedBiFunctionjava.util.function.BiFunction 等:

public interface Attempt {

    ......
        
    /**
     * 包裝受檢的 Consumer
     */
    static <T> Consumer<T> accept(CheckedConsumer<T> consumer) {
        Objects.requireNonNull(consumer);

        return t -> {
            try {
                consumer.accept(t);
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        };
    }

    /**
     * 包裝受檢的 Consumer,並自定義異常處理
     */
    static <T> Consumer<T> accept(CheckedConsumer<T> consumer, Consumer<Throwable> handler) {
        Objects.requireNonNull(consumer);
        Objects.requireNonNull(handler);

        return t -> {
            try {
                consumer.accept(t);
            } catch (Throwable e) {
                handler.accept(e);
            }
        };
    }
}
複製代碼

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

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

推薦

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

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

文末

歡迎關注我的微信公衆號:Coder編程 歡迎關注Coder編程公衆號,主要分享數據結構與算法、Java相關知識體系、框架知識及原理、Spring全家桶、微服務項目實戰、DevOps實踐之路、每日一篇互聯網大廠面試或筆試題以及PMP項目管理知識等。更多精彩內容正在路上~ 新建了一個qq羣:315211365,歡迎你們進羣交流一塊兒學習。謝謝了!也能夠介紹給身邊有須要的朋友。

文章收錄至 Github: github.com/CoderMerlin… Gitee: gitee.com/573059382/c… 歡迎關注並star~

微信公衆號
相關文章
相關標籤/搜索