翻譯 | Java流中如何處理異常

廣州蘆葦科技Java開發團隊發表於2019-01-22

原文自國外技術社群dzone,作者為 Brian Vermeer,傳送門

如果在 lambda 中你想要使用一個丟擲檢查性異常的方法時,你需要額外做一些事情。

流API和 lambda 是 Java8 之後的一個巨大進步。從那時開始,我們能夠使用更多函式式編碼方式來開發。現在,經過這幾年的程式碼建設,其中一個還遺留的大問題是如何在一個 lambda 表示式處理檢查性異常。

大體和你知道的那樣,在 lambda 中直接調起一個顯性丟擲檢查性異常的方法是不可能的。在某種程度上,我們需要捕獲這個異常使得程式碼能夠成功編譯。自然而然地,我們可以在 lambda 中使用一個簡單的 try-catch 並且封裝異常到RuntimeException中,就像下面的第一個例子一樣,但是我想大家都會認為這並不是最佳的方法。

myList.stream()
  .map(item -> {
    try {
      return doSomething(item);
    } catch (MyException e) {
      throw new RuntimeException(e);
    }
  })
  .forEach(System.out::println);
複製程式碼

大部分人都會認為這段 lambda 又笨重而且易讀性差,在編者的觀點中,這應該是儘可能預防的。如果我們想在超過一行的程式碼中搞事情,我們大可把方法體提取出來並放到一個獨立的方法中並且呼叫這個方法。一種更好並且更易讀的解決方法是將請求封裝到一個包含 try-catch 的簡單方法中,並且在你的 lambda 中呼叫這個方法。

myList.stream()
  .map(this::trySomething)
  .forEach(System.out::println);
private Item trySomething(Item item) {
  try {
    return doSomething(item);
  } catch (MyException e) {
    throw new RuntimeException(e);
  }
}
複製程式碼

這種解決方法至少變得可讀了並且分散了我們的關注點。如果你真的想捕獲異常,做一些特定的事項而不是簡單的將異常封裝到RuntimeException當中,這或許是一個可行而且可讀的解決方法。

執行時異常(RuntimeException)

在很多情況中,你會看到開發者會使用諸如此類的解決方法去重新包裝異常到RuntimeException當中或者對一個非檢查性異常的更具體的實現方法。通過這種做法,方法能夠在 lambda 中被呼叫並且被使用在更高階的方法當中。

這個做法個人認為關係不大(I can relate a bit to this practice)因為我覺得在通常情況下檢查性異常並沒有多大價值,但是這是另外一個討論內容了所以我不打算在這裡開展。如果你想巢狀所有包含RuntimeException檢查的 lambda 請求,你會發現這種方式會重複許多次。為了防止一遍又一遍地重寫相同的程式碼,為什麼不將其抽象為一個應用函式(utility function, was used by Joshua Bloch in the book Effective Java to describe the methods on classes such as Arrays, Objects and Math.)呢?通過這種方法,你只需要編寫一種程式碼並且在任何你需要的時候呼叫它。

要這樣做,首先你需要為你的方法編寫一個自定義的函式式介面(functional interface)。在這裡,你需要定義一個丟擲異常的方法。

@FunctionalInterface
public interface CheckedFunction<T,R> {
    R apply(T t) throws Exception;
}
複製程式碼

現在,你得準備編寫一個接收上面定義的介面CheckedFunction的應用函式。你能夠在函式裡面處理 try-catch 並且將異常包裝到RuntimeException當中(或者其他未檢查變型(unchecked variant))。我知道我們現在以一個難看的 lambda 體結束並且你也能夠將內容從這裡抽象出來。你自己做選擇看這個獨立的應用函式是否值得付出代價。

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 使其能丟擲異常。從這裡開始,所有事都變得可處理了。

myList.stream()
       .map(wrap(item -> doSomething(item)))
       .forEach(System.out::println);
複製程式碼

唯一遺留的問題是當異常發生了,流的處理會立刻停止。如果你覺得這樣沒問題,那就這樣做吧。但是我能想象這種直接中止的行為對許多情況並不可行。

Either

當使用流處理時,如果異常發生了,我們一般不希望程式中止。如果流上有非常大的一個資料需要被處理,你會希望當處理第二項的時候中止流處理嗎?大概都不想吧。

讓我們把思路轉回來。為什麼不盡可能多考慮”額外的情況“,就像我們想要一個”成功的“結果一樣。讓我們把這些情況當作資料,繼續對流進行處理,並且決定之後我們如何處理它。我們能夠處理它,但是想讓其成為可能,我們需要介紹一種新的型別 — Either 型別。

Either 型別是函式式語言一種常用的型別,並且當前並還沒成為 Java 的一部分。和 Java 中的 Optional 型別類似,一個Either相當於帶有兩種可能的通用包裝體,它可能是左也可能是右但永遠不可能都包含。無論是左還是右都可能是任意型別。例如,如果有一個 Either 變數,變數中可以持有 String 型別資料或者 Integer 型別的其中一個Either<String, Integer>

如果我們使用這個原則去處理異常,我們可以說我們的Either型別持有Exception或者另一變數。簡單來說,左邊是一個異常而右邊是執行成功的結果。你要記住這裡的右邊不僅僅是指左手邊也是指類似於"ok","good"的同義詞。

在下面,將會看到一個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變數而非丟擲一個異常。但是如果想在 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。我們回到原始問題上,現在我們能夠通過一連串的 Either 流來處理而不是一個使整個流毀壞的可能出現的RuntimeException

myList.stream()
       .map(Either.lift(item -> doSomething(item)))
       .forEach(System.out::println);
複製程式碼

這僅僅意味著我們拿回來主動權。通過使用流API的 filter 方法,我們能夠簡單地過濾左例項和記錄它們。你也能夠過濾右例項並且簡單地忽略掉異常情況。不管哪種方法,你都能重新掌握控制權並且你的流處理將不會被一個可能發生執行時異常導致立刻中止了。

因為Either是一個泛型包裝器,它能被運用在任意型別當中,而不是隻侷限於異常處理。這給到我們機會去處理事情而並不僅僅是將異常包裝到Either的左部分。我們現在遇到的問題是,如果Either僅僅儲存封裝的異常,並且我們會因為失去原來的資料而不能做一個重試。通過使用Either的能力來儲存所有資料,我們能夠儲存異常以及在左部的資料變數。為了達成目的,我們簡單地創造一個二次靜態 lift 方法。

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));
    }
  };
}
複製程式碼

可以看到在這個帶有 Pair型別liftWithValue是用於把異常和原始值組成已對放到Either的左部。現在,如果有異常產生我們能夠得到我們想要的所有資訊,而並不是僅僅只有異常。

這裡使用的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 內部中會丟擲異常的方法就變得更加靈活和可控制了。當Either在右部時,這時可以準確地執行並且將結果提取出來。在另一方面,如果Either在左方,我們能知道是在哪裡出現錯誤並且可以獲取異常及原數值,這樣我們就可以按我們的想法去處理事情了。通過使用Either型別代替將檢查性異常包裝到執行時異常中,我們就能夠防止流中途中斷了。

Try

有些開發人員在處理異常的時候,例如 Scala,會使用Try來代替EitherTry型別和Either型別非常相似。一樣地,它也有兩種情況:“成功”(success)或者“失敗”(failure)。failure 只能儲存異常型別,success 能夠儲存所有你想存放的型別。所以,Try型別只不過是左方(failure)適配為異常型別的Either的一種具體實現罷了。

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只能在 failure 部分控制異常本身,所以我認為還是會遇到在Either章節中第一部分的相同問題。我本人比較喜歡Either的靈活性。無論如何,當你使用Try或者Either,你都能解決異常處理中最初遇到的問題並且可以讓流能夠不被執行時異常中斷。

無論是Either還是Try,都是非常容易開發者自己去實現。在另一方面,你也能夠那些可用的功能性庫。例如,VAVR(原名為 Javaslang)已經將兩種型別都實現了並且有可用的輔助方法。我非常建議讀者們能夠瀏覽一下它不僅侷限於這兩種型別。然而,你必須思考是否真的需要這個龐大的三方庫,尤其只是用來處理那些通過少量的程式碼就可以實現的異常處理。

總結

當你想在 lambda 中使用一個丟擲檢查性異常的方法,你必須額外做一些事情。通過將其包裝到RuntimeException中是其中一種解決方法。如果讀者你更喜歡這種方法,我強烈推薦你們去編寫一個簡單的包裝工具並且重用它,這樣你就不需要為了每次去處理try/catch而煩惱了。

如果你想獲得更多的主動權,你能夠使用Either或者Try型別去包裝方法的輸出,因此你能夠將它處理成一段資料中。在它們的幫助下,流不會再因異常的丟擲而中斷並且你也能按照你的意願去處理流中的資料了。

譯者總結

這篇文章,譯者第一次快速瀏覽的時候以為翻譯難度並不大。但是在開始後,發現裡面出現的生詞以及難懂的語句特別多,也因此用了比較長的時間來處理。所以如果裡面有不通順或者和原文不對稱的地方,希望讀者能夠在下方評論指出,譯者我也會通過這來提高自己的能力。

說會到文章本身,譯者在寫流相關的程式碼的時候,對異常處理並沒有太過在意,讀了這篇文章,發現也可以通過函數語言程式設計的方法解決,相信對讀者應該也會有所幫助,那麼我們下次見。

小喇叭

廣州蘆葦科技Java開發團隊

蘆葦科技-廣州專業網際網路軟體服務公司

抓住每一處細節 ,創造每一個美好

關注我們的公眾號,瞭解更多

想和我們一起奮鬥嗎?lagou搜尋“ 蘆葦科技 ”或者投放簡歷到 server@talkmoney.cn 加入我們吧

翻譯 | Java流中如何處理異常

關注我們,你的評論和點贊對我們最大的支援

相關文章