JAVA 流不能很好地處理已檢查的異常。在本文中,深入探討如何管理此類問題。
Java 引入了檢查異常的概念。與早期的方法相比,強制開發(fā)人員管理異常的想法是革命性的。
如今,Java 仍然是唯一一種提供檢查異常的廣泛使用的語言。例如,Kotlin 中的每個異常都是未經檢查的。
即使在 Java 中,新特性也與受檢異常不一致:Java 內置函數式接口的簽名不使用異常。當將遺留代碼集成到 lambda 中時,會導致代碼繁瑣。這在 Streams 中很明顯。
在這篇文章中,我想深入探討如何處理此類問題。
代碼中的問題
下面是一個示例代碼來說明這個問題:
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(it -> new ForNamer().Apply(it)) // 1
.forEach(System.out::println);
- 不編譯:需要捕獲已檢查ClassNotFoundException
我們必須添加一個 try/catch 塊來修復編譯問題。
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(it -> {
try {
return Class.forName(it);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
})
.forEach(System.out::println);
添加塊破壞了易于閱讀的管道的目的。
將 Try/Catch 塊封裝到一個類中
為了恢復可讀性,我們需要重構代碼以引入一個新類。IntelliJ IDEA 甚至提出了一條記錄:
var forNamer = new ForNamer(); // 1
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(forNamer::apply) // 2
.forEach(System.out::println);
record ForNamer() implements Function> {
@Override
public Class apply(String string) {
try {
return Class.forName(string);
} catch (ClassNotFoundException e) {
return null;
}
}
}
- 創(chuàng)建單個記錄對象。
- 重復使用它。
嘗試龍目島
Project Lombok 是一個編譯時注釋處理器,可生成額外的字節(jié)碼。一個人使用正確的注釋并獲得結果,而無需編寫樣板代碼。
Project Lombok 是一個 Java 庫,可自動插入您的編輯器和構建工具,為您的 Java 增添趣味。永遠不要再編寫另一個 getter 或 equals 方法,使用一個注釋,您的類就有一個功能齊全的構建器、自動化您的日志記錄變量等等。
-龍目島計劃
Lombok 提供了@SneakyThrow注解:它允許人們拋出已檢查的異常,而無需在自己的方法簽名中聲明它們。然而,它目前不適用于現有的 API。
如果您是 Lombok 用戶,請注意有一個已打開的 GitHub 問題,其狀態(tài)為停放。
Commons Lang 救援
Apache Commons Lang是一個古老的項目。它在當時很普遍,因為它提供的實用程序可能是 Java API 的一部分,但不是。這是一個比在每個項目中重新發(fā)明你的DateUtils和更好的選擇。StringUtils在研究這篇文章時,我發(fā)現它仍然定期使用很棒的 API 進行維護。其中之一是FailableAPI。
API 由兩部分組成:
- 一個包裝器Stream
- 簽名接受異常的管道方法
這是一個小摘錄:
代碼終于變成了我們一開始就期待的樣子:
Stream stream = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList");
Failable.stream(stream)
.map(Class::forName) // 1
.forEach(System.out::println);
修復編譯時錯誤是不夠的
前面的代碼在運行時ClassNotFoundException拋出一個Wrapped in an 。我們滿足了編譯器,但我們無法指定預期的行為:UndeclaredThrowableException
- 拋出第一個異常
- 丟棄異常
- 聚合類和異常,以便我們可以在管道的最后階段對它們采取行動
- 別的東西
為了實現這一點,我們可以利用 Vavr 的力量。Vavr 是一個將函數式編程的強大功能帶入 Java 語言的庫:
Vavr 核心是 Java 的函數庫。它有助于減少代碼量并增加健壯性。函數式編程的第一步是開始思考不可變的值。Vavr 提供了不可變的集合以及對這些值進行操作所需的函數和控制結構。結果很漂亮,而且很有效。
- Vavr
想象一下,我們想要一個收集異常和類的管道。以下是描述幾個構建塊的 API 摘錄:
它轉換為以下代碼:
Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(CheckedFunction1.liftTry(Class::forName)) // 1
.map(Try::toEither) // 2
.forEach(e -> {
if (e.isLeft()) { // 3
System.out.println("not found:" + e.getLeft().getMessage());
} else {
System.out.println("class:" + e.get().getName());
- 將調用包裝到 VavrTry中。
- 將 轉換Try為Either以保留異常。如果我們不感興趣,我們可以使用 anOptional來代替。
- 根據是否Either包含異常left或預期結果right 采取行動。
到目前為止,我們還停留在 Java Streams 的世界中。它按預期工作,直到forEach看起來并不“好”。
Vavr 確實提供了自己的Stream類,它模仿 Java StreamAPI 并添加了額外的特性。讓我們用它來重寫管道:
var result = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList")
.map(CheckedFunction1.liftTry(Class::forName))
.map(Try::toEither)
.partition(Either::isLeft) // 1
.map1(left -> left.map(Either::getLeft)) // 2
.map2(right -> right.map(Either::get)); // 3
result._1().forEach(it -> System.out.println("not found: " + it.getMessage())); // 4
result._2().forEach(it -> System.out.println("class: " + it.getName())); // 4
- Stream將of劃分Either為兩個的元組Stream。
- 將左側流從 a Streamof展平Either到 a Streamof Throwable。
- 將右流從 a Streamof展平Either到 a Streamof Class。
- 做我們想做的任何事。
結論
Java 的初始設計大量使用了檢查異常。編程語言的發(fā)展證明這不是一個好主意。
Java 流不能很好地處理已檢查的異常。將后者集成到前者所需的代碼看起來不太好。為了恢復我們期望的流的可讀性,我們可以依賴 Apache Commons Lang。
匯編只代表了問題的一小部分。我們通常希望對異常采取行動,而不是停止管道或忽略異常。在這種情況下,我們可以利用 Vavr 庫,它提供了一種更實用的方法。
你可以在GitHub上找到這篇文章的源代碼。