我多次看到,當我們必須處理已檢查的例外時,在 java 中使用函式式 API 非常冗長且容易出錯。
例如:撰寫(更容易閱讀)代碼真的很方便
var obj = Objects.requireNonNullElseGet(something, Other::get);
實際上,它還避免了對 getter 的不當多次呼叫,例如當您這樣做時
var obj = something.get() != null ? something.get() : other.get();
// ^^^^ first ^^^^ ^^^^ second ^^^^
但是當你必須處理檢查例外時,一切都變成了叢林,我有時會看到這種非常丑陋的代碼風格:
try {
Objects.requireNonNullElseGet(obj, () -> {
try {
return invokeMethodWhichThrows();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException r){
Throwable cause = r.getCause();
if(cause == null)
throw r;
else
throw cause;
}
唯一的目的是處理檢查的例外,例如在沒有 lambda 的情況下撰寫代碼。現在,我知道這些情況可以用三元運算子和一個保存結果的變數更好地表達something.get(),但對于JDKObjects.requireNonNullElse(a, b)的java.util包中的 JDK 也是如此。
對于將供應商作為引數并僅在需要時評估它們的日志記錄框架的方法也可以這樣說,但是如果您需要處理這些供應商中的已檢查例外,則需要呼叫它們并顯式檢查日志級別。
if(LOGGER.isDebugEnabled())
LOGGER.debug("request from " resolveIPOrThrow());
一些類似的理由也可以是 s 的女仆Future,但讓我繼續。
我的問題是:為什么 Java 中的功能 API 不處理檢查例外?
例如ThrowingSupplier,像下面這樣的介面,可以潛在地滿足處理檢查例外、保證型別一致性和更好的代碼可讀性的需求。
interface ThrowingSupplier<O, T extends Exception> {
O get() throws T;
}
然后,我們需要復制使用 Suppliers 的方法以具有使用 ThrowingSuppliers 并引發例外的多載。但是我們作為 Java 開發人員已經習慣了這種重復(比如 with Stream, IntStream, LongStream, 或帶有多載的方法來處理int[], char[], long[], byte[], ...),所以這對我們來說并沒有什么奇怪的。
如果對 JDK 有深入了解的人爭論為什么檢查例外已從功能 API 中排除,我將非常感激,如果有辦法合并它們。
uj5u.com熱心網友回復:
這個問題可以解釋為“為什么做出這個決定的人會這樣決定”,它是在問:“請總結 5 年的嚴肅辯論——特別是 Brian Goetz 和他的想法”,這是不可能的,除非你名字是布賴恩·戈茨。據我所知,他沒有回答關于 SO 的問題。如果你愿意,你可以在 lambda-dev 郵件串列的 de 檔案中探索。
不過,人們可以做出明智的猜測。
范圍內與范圍外
lambdas沒有3 種透明度。
- 控制流。
- 檢查例外。
- 可變的區域變數。
控制流透明度
以這段代碼為例:
private Map<String, PhoneNumber> phonebook = ...;
public PhoneNumber findPhoneNumberOf(String personName) {
phonebook.entrySet().stream().forEach(entry -> {
if (entry.getKey().equals(personName)) return entry.getValue();
});
return null;
}
這段代碼很愚蠢(為什么不只做 a .get,或者如果我們必須通過這個東西,為什么不使用.filterand .findFirst,但如果你看過去,它甚至不起作用:你不能return從那個 lambda 中使用方法。那個return宣告回傳 lambda (因此是編譯器錯誤,您傳遞給的 lambdaforEach回傳void)。您也不能continue或break從其內部的 lambda 之外的回圈。
與可以做得很好的 for 回圈相比:
for (var entry : phonebook.entrySet()) {
if (entry.getKey().equals(personName)) return entry.getValue();
}
return null;
完全按照您的想法做,并且作業正常。
檢查例外透明度
這就是你抱怨的那個。這不編譯:
public void printFiles(Path... files) throws IOException {
Arrays.stream(files).forEach(p -> System.out.println(Files.readString(p)));
}
背景關系允許您拋出 IOExceptions 的事實無濟于事:以上內容無法編譯,因為“可以拋出 IOExceptions”作為狀態不會“轉移”到 lambda 內部。
這里有一個主題:將其重寫為普通的 for 回圈,它可以按照您想要的方式精確編譯和作業。那么,究竟為什么我們不能讓 lambda 以同樣的方式作業呢?
可變區域變數
這不起作用:
int x = 0;
someList.stream().forEach(k -> x );
System.out.println("Count: " x);
您既不能修改在 lambda 之外宣告的區域變數,也不能讀取它們,除非它們是(有效地)final。為什么不?
這些都是好東西..取決于范圍分層
到目前為止,lambda 在這三個方面不透明似乎真的很愚蠢。但在稍微不同的背景下,它變成了一件好事。想象一下,而不是.stream().forEach有點不同的東西:
class DoubleNullException extends Exception {} // checked!
public class Example {
private TreeSet<String> words;
public Example() throws DoubleNullException {
int comparisonCount = 0;
this.words = new TreeSet<String>((a, b) -> {
comparisonCount ;
if (a == null && b == null) throw new DoubleNullException();
});
System.out.println("Comparisons performed: " comparisonCount);
}
}
讓我們想象一下這 3 張透明膠片確實有效。上面的代碼使用了其中的兩個(嘗試 mutate comparisonCount,并嘗試DoubleNullException從內到外拋出)。
上面的代碼完全沒有意義。編譯器錯誤是非常需要的。該比較器可能要到下周才能在完全不同的執行緒中運行。每當您將第二個元素添加到集合中時,它就會運行,這是一個欄位,所以誰知道誰將執行此操作以及哪個執行緒將執行此操作。建構式早已停止運行 - 本地變數“在堆疊上”,因此本地變數已經消失。沒關系,這里的列印總是會列印 'comparisons made: 0',陳述句 'comparisonCount :' 將試圖增加一個不再保存該變數的記憶體位置。
即使我們“修復”了這個問題(編譯器意識到在 lambda 中使用了 local 并將其提升到堆上,這是大多數其他語言所做的),代碼作為一個概念仍然沒有意義:那個 print 陳述句不會列印。此外,可以從多個執行緒呼叫該比較器,所以......我們現在允許volatile在我們的本地變數上嗎?相當的蠕蟲罐頭!在當前的 java 中,區域變數不可能遇到執行緒并發同步問題,因為不可能與另一個執行緒共享變數(您可以共享變數指向的物件,而不是變數本身)。
您被允許(有效地)與最終本地人混淆的原因是因為您可以制作一個副本,這就是編譯器為您所做的。副本很好 - 如果沒有人改變任何東西。
該例外同樣不起作用:呼叫的代碼thatSet.add(someElement)將獲得DoubleNullException. 有人寫道:
Example ex;
try {
ex = new Example();
} catch (DoubleNullException e) {
throw new WrappedEx(e);
}
ex.add(null);
ex.add(null); // BOOM
帶有注釋 ( BOOM) 的行將拋出 DoubleNullEx。它“破壞”了檢查的例外規則:該行將編譯(set.add不拋出 DNEx),但不在允許拋出 DNEx 的背景關系中。上述代碼段中的 catch 塊永遠無法運行。
看看這一切是如何分崩離析的,沒有任何意義?
關鍵線索是:lambda 會發生什么?它是“運輸”的嗎?
在某些情況下,您將 lambda 直接交給一個方法,并且該方法具有“使用它并失去它”的心態:您將 lambda 交給該方法將運行它 0、1 或多次,但關鍵是:它會立即運行它,一旦您將 lambda 交給回傳的方法,該 lambda 就消失了。您將 lambda 交給的東西沒有將其存盤在欄位中或將其交給將其存盤在欄位中的其他代碼,該方法也沒有將 lambda 傳輸到另一個執行緒。
在這種情況下(方法是使用它然后丟失它),透明膠片肯定會很方便并且不會“破壞”任何東西。
但是,當您將 lambda 傳遞給的方法確實將其傳輸到一個欄位時(例如,其建構式TreeSet將傳遞的比較器存盤在一個欄位中,以便將來的.add呼叫可以呼叫它),透明度會分解并且沒有任何意義。
Java 中的 Lambda 兩者都適用,因此缺乏透明度(在所有 3 個方面)實際上是有道理的。當你有一個使用它然后失去它的情況時,這很煩人。
潛在的未來 JAVA 修復:我以前支持過它,但到目前為止,它大多被置若罔聞。下次我見到布賴恩時,我可能會再次提起它。想象一下,您可以將注釋或其他標記粘貼在方法的引數上,上面寫著:“我將使用它或丟失它”。然后編譯器將確保您不傳輸它(編譯器讓您使用該引數做的唯一事情就是呼叫.invoke()它。您不能呼叫其他任何東西,也不能將它分配或交給其他任何東西,除非您手它到一個方法,該方法也將該引數標記為@UseItOrLoseIt。然后編譯器可以通過對控制流和已檢查例外流進行一些戰術包裝來實作透明度,只需不抱怨(已檢查例外是javac的想象。運行時沒有檢查例外。這就是為什么 scala、kotlin 和其他運行在 JVM 上的語言可以做到這一點)。
實際上他們可以!
正如你的問題以 - 你實際上可以寫O get() throws T。那么為什么各種功能介面,比如Supplier,不這樣做呢?
主要是因為它很痛苦。老實說,我不確定為什么例如串列的 forEach未定義為:
public <T extends Throwable> forEach(ThrowingConsumer<? super E, ? super T> consumer) throws T {
for (E elem : this) consumer.consume(elem);
}
哪個可以正常作業并編譯(ThrowingConsumer具有明顯的 impl)。或者甚至Consumer像我們所擁有的那樣與<O, T extends Exception>零件一起宣告。
這有點麻煩。lambdas“作業”的方式是編譯器必須從背景關系中推斷出你正在實作的功能介面,特別是必須系結所有泛型。將例外系結添加到這種組合會使其變得更加困難。如果您正在“拋出 lambda” 中撰寫代碼并開始大量使用紅色下劃線,并且自動完成等沒有幫助,IDE 往往會有點困惑,因為 IDE 不能在它知道之前在這種情況下很有用。
Lambdas 作為一個系統也被設計為向后兼容地替換該概念的任何現有用法,例如 swing 的ActionListener. 這樣的偵聽器也不能拋出,因此使java.util.function包中的介面相似會更熟悉,并且可能更符合 java 習慣。
該throws T解決方案會有所幫助,但不是靈丹妙藥。它在一定程度上解決了檢查例外透明度的缺乏,但對于解決可變區域變數透明度或控制流透明度沒有任何作用。也許結論很簡單:這樣做的好處比你想象的要有限,成本比你想象的要高。成本/收益分析說:壞主意,所以沒有完成。
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/512973.html
標籤:爪哇例外功能接口
下一篇:確定例外原因
