技巧 | Java 8 Stream 中异常处理的4种方式
Stream API 和 lambda 是 Java8以來對Java的重大改進。從那時起,我們可以使用更具有功能性的語法風格的代碼。但是有個問題就是,我們使用了 lambda 表達式,那 lambda 中的異常該怎么處理呢。
大家都知道,不能直接在 lambda 中調用那些會拋出異常的方法,因為這樣從編譯上都通不過。所以我們需要捕獲異常以使代碼能夠編譯通過。
例如,我們可以在 lambda 中做一個簡單的 try-catch 并將異常包裝成一個 RuntimeException,如下面的代碼所示,但這不是最好的方法。
myList.stream().map(t -> {try {return doSomething(t);} catch (MyException e) {throw new RuntimeException(e);}}).forEach(System.out::println);我們大多數人都知道,lambda 代碼塊是笨重的,可讀性較差。在我看來,應該盡可能避免直接在 lambda 中使用大量的代碼段。
如果我們在 lambda 表達式中需要做多行代碼,那么我們可以把這些代碼提取到一個單獨的方法中,并簡單地調用新方法。
所以,解決此問題的更好和更易讀的方法是將調用包裝在一個普通的方法中,該方法執行 try-catch 并從 lambda 中調用該方法,如下面的代碼所示:
myList.stream().map(this::trySomething).forEach(System.out::println);private T trySomething(T t) {try {return doSomething(t);} catch (MyException e) {throw new RuntimeException(e);} }這個解決方案至少有點可讀性,并且將我們所關心的的問題也解決了。如果你真的想要捕獲異常并做一些特定的事情而不是簡單地將異常包裝成一個 RuntimeException,那么這對你來說可能是一個還不錯的解決方案。
一.包裝成運行時異常
在許多情況下,你會看到大家都喜歡將異常包裝成一個RuntimeException,或者是一個具體的未經檢查的異常類。這樣做的話,我們就可以在 lambda 內調用該方法。
如果你想把 lambda 中的每個可能拋出異常的調用都包裝到 RuntimeException中,那你會看到很多重復的代碼。為了避免一遍又一遍地重寫相同的代碼,我們可以將它抽象為一個方法,這樣,你只需編寫一次然后每次需要的時候直接調用他就可以了。
首先,你需要為函數編寫自己的方法接口。只有這一次,你需要定義該函數可能拋出異常,例如下列所示:
@FunctionalInterface public interface CheckedFunction<T,R> {R apply(T t) throws Exception; }現在,您可以編寫自己的通用方法了,它將接受一個 CheckedFunction 參數。你可以在這個通用方法中處理 try-catch 并將原始異常包裝到 RuntimeException中,如下列代碼所示:
public static <T,R> Function<T,R> wrap(CheckedFunction<T,R> checkedFunction) {return t -> {try {return checkedFunction.apply(t);} catch (Exception e) {throw new RuntimeException(e);}}; }但是這種寫法也是一個比較丑陋的 lambda 代碼塊,你可以選擇要不要再對方法進行抽象。
通過簡單的靜態導入,你現在可以使用全新的通用方法來包裝可能引發異常的lambda,如下列代碼所示:
myList.stream().map(wrap(t -> doSomething(t))).forEach(System.out::println);剩下的唯一問題是,當發生異常時,你的 stream 處理會立即停止。如果你的業務可以容忍這種情況的話,那沒問題,但是,我可以想象,在許多情況下,直接終止并不是最好的處理方式。
二.包裝成 Either 類型
使用 stream 時,如果發生異常,我們可能不希望停止處理。如果你的 stream 包含大量需要處理的項目,你是否希望在例如第二個項目引發異常時終止該 stream 呢?可能不是吧。
那我們可以換一種方式來思考,我們可以把 “異常情況” 下產生的結果,想象成一種特殊性的成功的結果。那我們可以把他們都看成是一種數據,不管成功還是失敗,都繼續處理流,然后決定如何處理它。我們可以這樣做,這就是我們需要引入的一種新類型 - Either類型。
Either 類型是函數式語言中的常見類型,而不是 Java 的一部分。與 Java 中的 Optional 類型類似,一個 Either 是具有兩種可能性的通用包裝器。它既可以是左派也可以是右派,但絕不是兩者兼而有之。左右兩種都可以是任何類型。
例如,如果我們有一個 Either 值,那么這個值可以包含 String 類型或 Integer 類型:Either。
如果我們將此原則用于異常處理,我們可以說我們的 Either 類型包含一個 Exception 或一個成功的值。為了方便處理,通常左邊是 Exception,右邊是成功值。
下面,你將看到一個 Either 類型的基本實現 。在這個例子中,我使用了 Optional 類型,代碼如下:
public class Either<L, R> {private final L left;private final R right;private Either(L left, R right) {this.left = left;this.right = right;}public static <L,R> Either<L,R> Left( L value) {return new Either(value, null);}public static <L,R> Either<L,R> Right( R value) {return new Either(null, value);}public Optional<L> getLeft() {return Optional.ofNullable(left);}public Optional<R> getRight() {return Optional.ofNullable(right);}public boolean isLeft() {return left != null;}public boolean isRight() {return right != null;}public <T> Optional<T> mapLeft(Function<? super L, T> mapper) {if (isLeft()) {return Optional.of(mapper.apply(left));}return Optional.empty();}public <T> Optional<T> mapRight(Function<? super R, T> mapper) {if (isRight()) {return Optional.of(mapper.apply(right));}return Optional.empty();}public String toString() {if (isLeft()) {return "Left(" + left +")";}return "Right(" + right +")";}}你現在可以讓你自己的函數返回 Either 而不是拋出一個 Exception。但是如果你想在現有的拋出異常的 lambda 代碼中直接使用 Either 的話,你還需要對原有的代碼做一些調整,如下所示:
public static <T,R> Function<T, Either> lift(CheckedFunction<T,R> function) {return t -> {try {return Either.Right(function.apply(t));} catch (Exception ex) {return Either.Left(ex);}}; }通過添加這種靜態提升方法 Either,我們現在可以簡單地“提升”拋出已檢查異常的函數,并讓它返回一個 Either。這樣做的話,我們現在最終得到一個 Eithers 流而不是一個可能會終止我們的 Stream 的 RuntimeException,具體的代碼如下:
myList.stream().map(Either.lift(item -> doSomething(item))).forEach(System.out::println);通過在 Stream APU 中使用過濾器功能,我們可以簡單地過濾出左側實例,然后打印日志。也可以過濾右側的實例,并且忽略掉異常的情況。無論哪種方式,你都可以對結果進行控制,并且當可能 RuntimeException 發生時你的流不會立即終止。
因為 Either 類型是一個通用的包裝器,所以它可以用于任何類型,而不僅僅用于異常處理。這使我們有機會做更多的事情而不僅僅是將一個 Exception 包裝到一個 Either 的左側實例中。
我們現在可能遇到的問題是,如果 Either 只保存了包裝的異常,并且我們無法重試,因為我們丟失了原始值。
通過使用 Either 保存任何東西的能力,我們可以將異常和原始值都保存在左側。為此,我們只需制作第二個靜態提升功能。
public static <T,R> Function<T, Either> liftWithValue(CheckedFunction<T,R> function) {return t -> {try {return Either.Right(function.apply(t));} catch (Exception ex) {return Either.Left(Pair.of(ex,t));}}; }你可以看到,在這個 liftWithValue 函數中,這個 Pair 類型用于將異常和原始值配對到 Either 的左側,如果出現問題我們可能需要所有信息,而不是只有 Exception。
Pair 使用的類型是另一種泛型類型,可以在 Apache Commons lang 庫中找到,或者你也可以簡單地實現自己的類型。
無論如何,它只是一個可以容納兩個值的類型,如下所示:
public class Pair<F,S> {public final F fst;public final S snd;private Pair(F fst, S snd) {this.fst = fst;this.snd = snd;}public static <F,S> Pair<F,S> of(F fst, S snd) {return new Pair<>(fst,snd);}}通過使用 liftWithValue,你現在可以靈活的并且可控制的來在 lambda 表達式中調用可能會拋出 Exception 的方法了。
如果 Either 是一個 Right 類型,我們知道我們的方法已正確執行,我們可以正常的提取結果。另一方面,如果 Either 是一個 Left 類型,那意味著有地方出了問題,我們可以提取 Exception 和原始值,然后我們可以按照具體的業務來繼續處理。
通過使用 Either 類型而不是將被檢查包裝 Exception 成 RuntimeException,我們可以防止 Stream 中途終止。
三.包裝成 Try 類型
使用過 Scala 的人可能會使用 Try 而不是 Either 來處理異常。Try 類型與 Either 類型是非常相似的。
它也有兩種情況:“成功”或“失敗”。失敗時只能保存 Exception 類型,而成功時可以保存任何你想要的類型。
所以 Try 可以說是 Either 的一種固定的實現,因為他的 Left 類型被確定為 Exception了,如下列的代碼所示:
public class Try<Exception, R> {private final Exception failure;private final R succes;public Try(Exception failure, R succes) {this.failure = failure;this.succes = succes;}}有人可能會覺得 Try 類型更加容易使用,但是因為 Try 只能將 Exception 保存在 Left 中,所以無法將原始數據保存起來,這就和最開始 Either 不使用 Pair 時遇到的問題一樣了。所以我個人更喜歡 Either 這種更加靈活的。
無論如何,不管你使用 Try 還是 Either,這兩種情況,你都解決了異常處理的初始問題,并且不要讓你的流因為 RuntimeException而終止。
四.使用已有的工具庫
無論是 Either 和 Try 是很容易實現自己。另一方面,您還可以查看可用的功能庫。例如:VAVR(以前稱為Javaslang)確實具有可用的類型和輔助函數的實現。我建議你去看看它,因為它比這兩種類型還要多得多。
但是,你可以問自己這樣一個問題:當你只需幾行代碼就可以自己實現它時,是否希望將這個大型庫作為依賴項進行異常處理。
結論
當你想在 lambda 表達式中調用一個會拋出異常的方法時,你需要做一些額外的處理才行。
-
將其包裝成一個 RuntimeException 并且創建一個簡單的包裝工具來復用它,這樣你就不需要每次都寫try/catch 了
-
如果你想要有更多的控制權,那你可以使用 Either 或者 Try 類型來包裝方法執行的結果,這樣你就可以將結果當成一段數據來處理了,并且當拋出 RuntimeException 時,你的流也不會終止。
-
如果你不想自己封裝 Either 或者 Try 類型,那么你可以選擇已有的工具庫來使用
總結
以上是生活随笔為你收集整理的技巧 | Java 8 Stream 中异常处理的4种方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式事务不理解?一次给你讲清楚!
- 下一篇: 看看这些大龄程序员都做了些什么