Java8(1):當(dāng) Lambda 遇上受檢異常

我今天高高興興询枚,想寫個簡單的統(tǒng)計一個項目下有多少行代碼的小程序识腿,于是咔咔的寫下:

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("代碼行數(shù):" + count);

{ 題外話開始:

  • Files.walk(Path) 在 JDK1.8 時添加赁豆,深度優(yōu)先遍歷一個 Path (目錄)汤功,返回這個目錄下所有的 Path(目錄和文件),通過 Stream<Path> 返回导坟;

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

題外話結(jié)束 }

然后惫周,編譯不過 —— 因為 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("代碼行數(shù):" + count);

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

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

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("代碼行數(shù):" + 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)。但每次都為了一行代碼去新建一個方法酵熙,與我們的初衷就相悖了 —— 因為我們想要的是簡潔轧简,不是麻煩。

解決方法2:將會拋出異常的函數(shù)進行包裝匾二,使其不拋出受檢異常


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


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

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

那么該 FunctionalInterface 便可以作為類似于 file -> File.lines(file) 這類會拋出受檢異常的 Lambda 的目標(biāo)類型察藐,此時 Lambda 中并不需要捕獲異常(因為目標(biāo)類型的 apply 方法已經(jīng)將異常拋出了)—— 之所以原來的 Lambda 需要捕獲異常皮璧,就是因為在流式操作 flatMap 中使用的 java.util.function 包下的 Function<T, R> 沒有拋出異常:

java.util.function.Function

那我們?nèi)绾问褂?CheckedFunction 到流式操作的 Lambda 中呢?
首先我們定義一個 Attempt 接口分飞,它的 apply 靜態(tài)方法提供將 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("代碼行數(shù):" + count);

此時,我們便可以選擇是否去捕獲異常(RuntimeException)譬猫。這種解決方法下讯檐,我們一般不關(guān)心拋出異常的情況 —— 比如自己寫的小例子,拋出了異常程序就該終止染服;或者你知道這個 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 類的時候出了問題,我們可以就假設(shè)這個文件的行數(shù)為 0 秉颗,那么默認(rèn)值就是個空的 Stream<String>(當(dāng)然你也可以選擇順手記錄一下異常):

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("代碼行數(shù):" + count);

使用 CheckedFunction 這種方式更為通用 —— 類似的痢毒,我們可以包裝 CheckedConsumerjava.util.function.Consumer,包裝 CheckedSupplierjava.util.function.Suppiler站宗,CheckedBiFunctionjava.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 接口的代碼可見:Attempt.java


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子闽铐,更是在濱河造成了極大的恐慌蝶怔,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兄墅,死亡現(xiàn)場離奇詭異踢星,居然都是意外死亡,警方通過查閱死者的電腦和手機隙咸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門沐悦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人五督,你說我怎么就攤上這事藏否。” “怎么了充包?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵副签,是天一觀的道長。 經(jīng)常有香客問我误证,道長继薛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任愈捅,我火速辦了婚禮,結(jié)果婚禮上慈鸠,老公的妹妹穿的比我還像新娘蓝谨。我一直安慰自己,他們只是感情好青团,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布譬巫。 她就那樣靜靜地躺著,像睡著了一般督笆。 火紅的嫁衣襯著肌膚如雪芦昔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天娃肿,我揣著相機與錄音咕缎,去河邊找鬼。 笑死料扰,一個胖子當(dāng)著我的面吹牛凭豪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晒杈,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼嫂伞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起帖努,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撰豺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拼余,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體污桦,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年姿搜,在試婚紗的時候發(fā)現(xiàn)自己被綠了寡润。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡舅柜,死狀恐怖梭纹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情致份,我是刑警寧澤变抽,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站氮块,受9級特大地震影響绍载,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滔蝉,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一击儡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蝠引,春花似錦阳谍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吊洼,卻和暖如春训貌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背冒窍。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工递沪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人超燃。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓区拳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親意乓。 傳聞我的和親對象是個殘疾皇子樱调,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容