今天桨踪,我們將討論一個非常重要的主題-Java 中的異常處理。盡管有時可能會對此主題進(jìn)行過多的討論芹啥,但并非每篇文章都包含有用且相關(guān)的信息锻离。
Java 中最常見的異常處理機(jī)制通常與 try-catch 塊關(guān)聯(lián) 铺峭。我們使用它來捕獲異常,然后提供在發(fā)生異常的情況下可以執(zhí)行的邏輯汽纠。
的確卫键,你不需要將所有異常都放在這些塊中。另一方面虱朵,如果你正在研究應(yīng)用程序的軟件設(shè)計莉炉,則可能不需要內(nèi)置的異常處理機(jī)制。在這種情況下碴犬,你可以嘗試使用替代方法-Vavr Try 結(jié)構(gòu)絮宁。
在本文中,我們將探討 Java 異常處理的不同方法服协,并討論如何使用 Vavr Try 替代內(nèi)置方法绍昂。讓我們開始吧!
處理 Java 中的異常
作為介紹偿荷,讓我們回顧一下 Java 如何允許我們處理異常窘游。如果你不記得它,則 Java 中的異常會指出意外或意外事件跳纳,該異常在程序執(zhí)行期間(即在運(yùn)行時)發(fā)生忍饰,這會破壞程序指令的正常流程。Java為我們提供了上述 try-catch 捕獲異常的機(jī)制寺庄。讓我們簡要檢查一下它是如何工作的艾蓝。
如果不處理異常會發(fā)生什么?
首先铣揉,讓我們看一個非常常見的例子饶深。這是一個包含 JDBC 代碼的代碼段:
Connection connection = dataSource.getConnection();
String updateNameSql = "UPDATE employees SET name=? WHERE emp_id=?";
PreparedStatement preparedStatement = connection.prepareStatement(updateNameSql);
坦白地說,你的 IDE 甚至不允許你編寫這段代碼逛拱,而是要求用 try-catch 塊將其包圍敌厘,像這樣:
try {
Connection connection = dataSource.getConnection();
String updateNameSql = "UPDATE employees SET name=? WHERE emp_id=?";
PreparedStatement preparedStatement = connection.prepareStatement(updateNameSql);
} catch (SQLException ex){
}
注:我們可以將其重構(gòu)為 try-with-resources,但是稍后再討論朽合。
那么俱两,為什么我們要這樣編寫代碼?因為 SQLException 是一個檢查異常曹步。
如果這些異诚懿剩可以由方法或構(gòu)造函數(shù)的執(zhí)行拋出并傳播到方法或構(gòu)造函數(shù)邊界之外,則必須在方法或構(gòu)造函數(shù)的 throws 子句中聲明這些異常讲婚。SQLException 如果發(fā)生數(shù)據(jù)庫訪問錯誤尿孔,則在示例中使用的方法將拋出 。因此,我們用一個 try-catch 塊將其包圍活合。
Java 在編譯過程中驗證了這些異常雏婶,這就是它們與運(yùn)行時異常不同的原因。
但是你不必處理所有異常情況
但是白指,并非每個異常都應(yīng)被一個 try-catch 塊包圍留晚。
情況 1:運(yùn)行時異常
Java 異常是 Throwable 的子類,但是其中一些是 RuntimeException 類的子類告嘲〈砦看下面的圖,它給出了 Java 異常的層次結(jié)構(gòu):
請注意橄唬,運(yùn)行時異常是特定的組赋焕。根據(jù) Java 規(guī)范,從這些異常中還是有可能恢復(fù)的轧坎。作為示例宏邮,讓我們回想一下 ArrayIndexOutOfBoundsException泽示「籽看看下面的示例代碼片段:
int numbers[] = [1,43,51,0,9];
System.out.println(numbers[6]);
在這里,我們有一個具有5個值(0-4位)的整數(shù)數(shù)組械筛。當(dāng)我們嘗試檢索絕對超出范圍的值(索引= 6)時捎泻,Java 將拋出 ArrayIndexOutOfBoundsException。
這表明我們嘗試調(diào)用的索引為負(fù)數(shù)埋哟,大于或等于數(shù)組的大小笆豁。如我所說,這些異吵嗌蓿可以修復(fù)闯狱,因此在編譯過程中不會對其進(jìn)行檢查。這意味著你仍然可以編寫如下代碼:
int numbers[] = [1,43,51,0,9];
int index = 6;
try{
System.out.println(numbers[index]);
} catch (ArrayIndexOutOfBoundsException ex){
System.out.println("Incorrect index!");
}
但是你不必這樣做抛计。
情況 2:錯誤
Error 是另一個棘手的概念哄孤。再看一下上面的圖-存在錯誤,但是通常不會處理吹截。為什么瘦陈?通常,這是由于 Java 程序無法執(zhí)行任何操作來從錯誤中恢復(fù)波俄,例如:錯誤表明嚴(yán)重的問題晨逝,而合理的應(yīng)用程序甚至不應(yīng)嘗試捕獲。
讓我們考慮一下內(nèi)存錯誤– java.lang.VirtualMachineError懦铺。此錯誤表明 JVM 已損壞或已經(jīng)用盡了繼續(xù)運(yùn)行所必需的資源捉貌。換句話說,如果應(yīng)用程序的內(nèi)存不足,則它根本無法分配額外的內(nèi)存資源趁窃。
當(dāng)然苍匆,如果由于持有大量應(yīng)釋放的內(nèi)存而導(dǎo)致失敗,則異常處理程序可以嘗試釋放它(不是直接釋放它本身棚菊,而是可以調(diào)用JVM來釋放它)浸踩。并且,盡管這樣的處理程序在這種情況下可能有用统求,但是這樣的嘗試可能不會成功检碗。
Try-Catch 塊的變體
上述編寫 try-catch 語句的方法并不是 Java 中唯一可用的方法。還有其他方法:try-with-resources码邻,try-catch-finally 和多個 catch 塊折剃。讓我們快速瀏覽這些不同的方法。
方法 1:Try-With-Resources
try-with-resources 塊在 Java 7 中引入的像屋,并允許開發(fā)者在程序運(yùn)行到此結(jié)束后必須關(guān)閉聲明的資源怕犁。我們可以在實現(xiàn)該 AutoCloseable 接口(即特定標(biāo)記接口)的任何類中包含資源。我們可以像這樣重構(gòu)所提到的 JDBC 代碼:
try (Connection connection = dataSource.getConnection){
String updateNameSql = "UPDATE employees SET name=? WHERE emp_id=?";
PreparedStatement preparedStatement = connection.prepareStatement(updateNameSql);
} catch (SQLException ex){
//..
}
Java 確保我們 Connection 在執(zhí)行代碼后將其關(guān)閉己莺。在進(jìn)行此構(gòu)建之前奏甫,我們必須顯式地關(guān)閉 finally 塊中的資源。
方法 2:Try + Finally
finally 塊在任何情況下都將執(zhí)行凌受。例如在成功情況下或在異常情況下阵子。在其中,你需要放置將在之后執(zhí)行的代碼:
FileReader reader = null;
try {
reader = new FileReader("/text.txt");
int i=0;
while(i != -1){
i = reader.read();
System.out.println((char) i);
}
} catch(IOException ex1){
//...
} finally{
if(reader != null){
try {
reader.close();
} catch (IOException ex2) {
//...
}
}
}
請注意胜蛉,此方法有一個缺點:如果在 finally 塊內(nèi)引發(fā)異常 挠进,則會使其中斷。因此誊册,我們必須正常處理異常领突。將 try-with-resources 與可關(guān)閉的資源一起使用,避免在 finally 塊內(nèi)關(guān)閉資源 案怯。
方法 3:多 Catch 塊
最后君旦,Java 允許我們使用一個 try-catch 塊多次捕獲異常。當(dāng)方法拋出幾種類型的異常并且您想?yún)^(qū)分每種情況的邏輯時殴泰,這很有用于宙。舉個例子,讓這個虛構(gòu)的類使用拋出多個異常的方法:
class Repository{
void insert(Car car) throws DatabaseAccessException, InvalidInputException {
//...
}
}
//...
try {
repository.insert(car);
} catch (DatabaseAccessException dae){
System.out.println("Database is down!");
} catch (InvalidInputException iie){
System.out.println("Invalid format of car!");
}
在這里需要記住什么悍汛?通常捞魁,我們假設(shè)在此代碼中,這些異常處于同一級別离咐。但是你必須從最具體到最一般的順序排序 catch 塊谱俭。例如奉件,捕獲 ArithmeticException 異常必須在 捕獲 Exception 異常之前。
到這里昆著,我們已經(jīng)回顧了如何使用內(nèi)置方法處理 Java 中的異诚孛玻。現(xiàn)在凑懂,讓我們看一下如何使用 Vavr 庫執(zhí)行此操作煤痕。
Vavr Try
我們回顧了捕獲 Java 異常的標(biāo)準(zhǔn)方法。另一種方法是使用 Vavr Try 類接谨,Vavr 是 Java 8+ 中一個函數(shù)式庫摆碉,提供了一些不可變數(shù)據(jù)類型及函數(shù)式控制結(jié)構(gòu)。首先脓豪,添加 Vavr 庫依賴項:
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.2</version>
</dependency>
Try 容器
Vavr 包含的 Try 類是 monadic 容器類型巷帝,它表示可能導(dǎo)致異常或返回成功計算出的值的計算扫夜。此結(jié)果可以采用 Success 或 Failure楞泼。看下面這段代碼:
class CarsRepository{
Car insert(Car car) throws DatabaseAccessException {
//...
}
Car find (String id) throws DatabaseAccessException {
//..
}
void update (Car car) throws DatabaseAccessException {
//..
}
void remove (String id) throws DatabaseAccessException {
//..
}
}
在調(diào)用此代碼時笤闯,我們將使用這些 try-catch 塊來處理 DatabaseAccessException堕阔。但是另一個解決方案是使用 Vavr 對其進(jìn)行重構(gòu)。查看以下代碼片段:
class CarsVavrRepository{
Try<Car> insert(Car car) {
System.out.println("Insert a car...");
return Try.success(car);
}
Try<Car> find (String id) {
System.out.println("Finding a car...");
System.out.println("..something wrong with database!");
return Try.failure(new DatabaseAccessException());
}
Try<Car> update (Car car) {
System.out.println("Updating a car...");
return Try.success(car);
}
Try<Void> remove (String id) {
System.out.println("Removing a car...");
System.out.println("..something wrong with database!");
return Try.failure(new DatabaseAccessException());
}
}
現(xiàn)在望侈,我們可以使用 Vavr 處理數(shù)據(jù)庫問題印蔬。
處理成功
當(dāng)我們收到成功計算的結(jié)果時,我們會收到 Success:
@Test
void successTest(){
CarsVavrRepository repository = new CarsVavrRepository();
Car skoda = new Car("skoda", "9T4 4242", "black");
Car result = repository.insert(skoda).getOrElse(new Car("volkswagen", "3E2 1222", "red"));
Assertions.assertEquals(skoda.getColor(), result.getColor());
Assertions.assertEquals(skoda.getId(), result.getId());
}
請注意脱衙,Vavr.Try 相較于 Vavr.Option,為我們提供了一種方便的 getOrElse 方法例驹,在發(fā)生故障的情況下我們可以使用默認(rèn)值捐韩,我們可以將這種邏輯與有問題的方法結(jié)合使用,例如與 find 一起使用鹃锈。
處理失敗
在另一種情況下荤胁,我們將處理 Failure:
@Test
void failureTest(){
CarsVavrRepository repository = new CarsVavrRepository();
// desired car
Car bmw = new Car("bmw", "4A1 2019", "white");
// failure car
Car failureCar = new Car("seat", "1A1 3112", "yellow");
Car result = repository.find("4A1 2019").getOrElse(failureCar);
Assertions.assertEquals(bmw.getColor(), result.getColor());
Assertions.assertEquals(bmw.getId(), result.getId());
}
運(yùn)行此代碼。由于斷言錯誤屎债,該測試將失斀稣:
org.opentest4j.AssertionFailedError:
Expected :white
Actual :yellow
這意味著因為我們在 find 方法中對失敗進(jìn)行了硬編碼 ,所以我們收到了默認(rèn)值盆驹。除了返回默認(rèn)值之外圆丹,我們還可以在發(fā)生錯誤的情況下執(zhí)行其他操作并生成結(jié)果。你可以使用鏈接函數(shù) Option 來使您的代碼更具功能性:
repository.insert(bmw).andThen(car -> {
System.out.println("Car is found "+car.getId());
}).andFinally(()->{
System.out.println("Finishing");
});
或者躯喇,你可以使用收到的異常執(zhí)行代碼辫封,如下所示:
repository.find("1A9 4312").orElseRun(error->{
//...
});
一般來說硝枉,Vavr Try 是一個功能豐富的解決方案,可用于以更實用的方式轉(zhuǎn)換代碼庫倦微。毫無疑問妻味,它與其他 Vavr 類(如 Option 或 Collections)結(jié)合后,才可以釋放出真正的力量欣福。但是责球, 如果您想編寫更多的功能樣式的代碼,即使沒有它們拓劝,Vavr Try 對于 Java 的 try-catch 塊來說也是一個的正確的替代選擇棕诵。
總結(jié)
Java 中的異常處理機(jī)制通常與 try-catch 塊關(guān)聯(lián), 以便捕獲異常并提供發(fā)生異常時將要執(zhí)行的邏輯凿将。同樣校套,我們確實不需要將所有異常都放入這些塊中。在本文中牧抵,我們探討了如何使用 Vavr 庫執(zhí)行此操作笛匙。