當程序出現錯誤,系統會自動拋出異常;除此之外,Java也允許程序自行拋出異常,自行拋出異常使用throw語句來完成。
一、拋出異常
系統是否拋出異常,可能需要根據業務需求來決定,如果程序中的數據、執行與既定的業務需求不符,這是一種異常。由於與業務需求不符而產生的異常,必須由程序員來決定拋出,系統無法拋出這種異常。
如果需要在程序中自行拋出異常,則應該使用throw語句,throw語句拋出的不是異常類,而是一個異常類的實例,而且每次都只能拋出一個異常實例。throw語法的格式:
throw ExceptionInstance;
使用throw語句改寫五子棋游戲處理用戶輸入的代碼:
try
{
// 將用戶輸入的字符串以逗號作為分隔符,分解成2個字符串
String[] posStrArr = inputStr.split(",");
// 將2個字符串轉換成用戶下棋的坐標
var xPos = Integer.parseInt(posStrArr[0]);
var yPos = Integer.parseInt(posStrArr[1]);
// 把對應的數組元素賦為"●"。
if (!gb.board[xPos - 1][yPos - 1].equals("╋"))
{
throw new Exception("你試圖下棋的坐標點已經有棋了");
}
gb.board[xPos - 1][yPos - 1] = "●";
}
catch (Exception e)
{
System.out.println("您輸入的坐標不合法,請重新輸入,"
+ "下棋坐標應以x,y的格式");
continue;
}
上面程序中throw new Exception("你試圖下棋的坐標點已經有棋了");拋出異常,程序認為當用戶試圖向一個已經有棋子的坐標點下棋就是異常。當Java允許時接收到開發者自行拋出異常時,同樣會中止當前流,跳動該異常對應的catch塊,由該catch塊來處理該異常。
如果throw拋出的異常是Checked異常,則該throw語句要么處在try塊中,顯式捕獲該異常,要么放在一個帶throws聲明拋出的方法中,即把該異常交給方法的調用者處理;如果throw語句拋出的異常是Runtime異常,那么該語句無須放在try塊中,也無須放在帶throws聲明拋出的方法中;程序即可以顯式使用try...catch來捕獲並處理該異常,也可以完全不理會該異常,將異常交給該方法的調用者處理:
public class ThrowTest
{
public static void main(String[] args)
{
try
{
// 調用聲明拋出Checked異常的方法,要么顯式捕獲該異常
// 要么在main方法中再次聲明拋出
throwChecked(-3);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
// 調用聲明拋出Runtime異常的方法既可以顯式捕獲該異常,
// 也可不理會該異常
throwRuntime(3);
}
public static void throwChecked(int a) throws Exception
{
if (a > 0)
{
// 自行拋出Exception異常
// 該代碼必須處於try塊里,或處於帶throws聲明的方法中
throw new Exception("a的值大於0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
// 自行拋出RuntimeException異常,既可以顯式捕獲該異常
// 也可完全不理會該異常,把該異常交給該方法調用者處理
throw new RuntimeException("a的值大於0,不符合要求");
}
}
}
---------- 運行Java捕獲輸出窗 ----------
Exception in thread "main" java.lang.RuntimeException: a的值大於0,不符合要求
at ThrowTest.throwRuntime(ThrowTest.java:38)
at ThrowTest.main(ThrowTest.java:19)
輸出完成 (耗時 0 秒) - 正常終止
通過上面的程序可以看出,自行拋出Runtime異常比自行拋出Checked異常的靈活性更好。同樣,通過Checked異常可以讓編譯器提醒程序員必須處理該異常。
二、自定義異常類
用戶自定義異常都應該繼承Exception基類,如果希望自定義Runtime異常,則應該繼承RuntimeException基類。定義異常類時需要提供兩個構造器:一個十五參數的構造器;另一個是帶有一個字符串參數的構造器,這個字符串作為該異常該異常對象的描述信息(也就是異常對象的getMessage()方法的返回值)。
public class AuctionException extends Exception
{
//無參數構造器器
public AuctionException(){}
//帶有一個字符串參數的構造器
public AuctionException(String msg)
{
super(msg);
}
}
上面創建了一個AuctionException異常類,並未該異常類提供了兩個構造器。其中帶有參數的構造器,僅通過super調用父類構造器,正是這行super代碼可以將字符串參數傳給異常對象的message屬性,該message屬性就是對該異常對象的詳細描述信息。
如果需要自定義Runtime異常,只需要將AuctionException.java程序中的Exception基類改為RuntimeException基類,其他地方無須修改。
三、catch和throw同時使用
處理異常的兩種方式:
1、在出現異常的方法內捕獲並處理異常,該方法的調用者將不能再次捕獲該異常。
2、該方法簽名中聲明拋出該異常,將異常完全交給方法調用者處理
當一個異常出現時,單靠某個方法無法完全處理該異常,必須有幾個方法協作才可以完全處理。也就是說,在異常出現的當前方法中,程序只對異常進行部分處理,還有些處理需要在該方法的調用者才能完成,所以該再次拋出異常,讓該方法的調用者也能捕獲到該異常。
為了實現這種通過多個方法協作處理同一個情形,可以在catch塊中結合throw語句來完成。下面展示這種catch和throw同時使用的方法:
public class AuctionTest
{
private double initPrice=30.0;
//因為該方法中顯式拋出了AuctionException異常
//所以此處需要聲明拋出AuctionException異常
public void bid(String bidPrice)
throws AuctionException
{
var d=0.0;
try
{
d=Double.parseDouble(bidPrice);
}
catch (Exception e)
{
//此處完成本方法中對異常執行的修復處理
//此處僅僅在控制台打印異常的跟蹤棧信息
e.printStackTrace();
//再次拋出自定義異常
throw new AuctionException("競拍價必須是整數,不能包含其他字符!");
}
if(initPrice>d)
{
throw new AuctionException("競拍起價比拍價低,不允許競拍!");
}
initPrice=d;
}
public static void main(String[] args)
{
var at=new AuctionTest();
try
{
at.bid("df");
}
catch (AuctionException ae)
{
//再次捕獲bid()方法中的異常,並對該異常進行處理
System.out.println(ae.getMessage());
}
}
}
---------- 運行Java捕獲輸出窗 ----------
java.lang.NumberFormatException: For input string: "df"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:549)
at AuctionTest.bid(AuctionTest.java:13)
at AuctionTest.main(AuctionTest.java:34)
競拍價必須是整數,不能包含其他字符!
輸出完成 (耗時 0 秒) - 正常終止
上面程序中catch塊捕獲到異常后,系統打印異常的跟蹤棧信息,接着拋出一個AuctionException異常,通知該方法的調用者再次處理該AuctionException異常.
四、使用throw語句拋出異常
try
{
new FileOutputStream("a.txt");
}
catch (Exception ex)
{
ex.printStackTrace();
throw ex;//①
}
上面代碼再次拋出了捕獲的異常,但這個異常對象的情況比較特殊:程序捕獲該異常,聲明該異常的類型為Exception;但實際上try塊中可能只調用了FileOutputStream構造器,這個構造器聲明只是拋出FileNotFoundException異常。
在Java7以前,編譯器處理“簡單而粗暴”——由於在捕獲該異常時聲明ex的類型是Exception。因此Java編譯器認為這段代碼可能拋出Exception異常,所以包含這段代碼的方法通常需要聲明拋出Exception異常。
從Java 7開始,Java編譯器會執行更加細致的檢查,Java編譯器會檢查throw語句拋出異常的實際類型,這樣編譯器知道①號代碼實際只能拋出FileNotFoundException異常,因此在方法簽名中只要聲明拋出FileNotFoundException異常。
import java.io.*;
public class ThrowsTest2
{
public static void main(String[] args)
//java 6認為①號代碼只能拋出Exception
//所以此處必須聲明拋出Exception
//Java 7會檢查①號代碼可能拋出異常的實際類型
//因此此處只需要聲明拋出FileNotFoundException異常即可
throws FileNotFoundException
{
try
{
var fis=new FileInputStream("a.txt");
}
catch (Exception ex)
{
ex.printStackTrace();
throw ex;//①
}
}
}
---------- 運行Java捕獲輸出窗 ----------
java.io.FileNotFoundException: a.txt (系統找不到指定的文件。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
at ThrowsTest2.main(ThrowsTest2.java:13)
Exception in thread "main" java.io.FileNotFoundException: a.txt (系統找不到指定的文件。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
at ThrowsTest2.main(ThrowsTest2.java:13)
輸出完成 (耗時 0 秒) - 正常終止
五、異常鏈
對於真實的企業級應用而言,常常有嚴格的分層關系,層與層之間有非常清晰的划分,上次功能的實現嚴格依賴於下層的API,也不會跨層訪問。下圖顯示這種具有分層結構應用的大致示意圖:
對於一個采用上圖所示的結構應用,當業務邏輯層出現SQLException異常時,程序不應該把底層的SQLException異常傳到用戶界面,有如下兩個原因:
1、對於正常用戶而言,它們不想看到底層的SQLException異常,SQLException異常對於他們使用該系統沒有任何幫助。
2、對於惡意用戶而言,將SQLException異常暴露出來不安全。
通常的做法:程序先捕獲原始異常,然后拋出一個新的業務異常,新的業務異常中包含了對用戶提示信息,這種處理方式被稱為異常轉譯。
假設程序需要實現工資計算的方法,程序應該采用如下結構的代碼來實現該方法:
public void calSal() throws SalException
{
try
{
//實現結算工資的業務
...
}
catch (SQLException sqle)
{
//把原始異常記錄下來,留給管理員
...
//下面的message就是對用戶的提示
throw new SalException("訪問底層數據庫出現問題");
}
catch (Exception e)
{
//把原始異常記錄下來,留給管理員
...
//下面的message就是對用戶的提示
throw new SalException("系統出現未知異常");
}
}
這種把異常信息異常起來,僅向上提供必要的提示信息處理方式,可以保證底層異常不會擴散到表現層,可以避免向上暴露太多的實現細節,這完全符合面向對象的封裝原則。
這種捕獲一種異常然后接着拋出另一個異常,並把原始異常信息保存下來是一種典型的鏈式處理方式(23種設計模式之一:職責鏈模式),也稱為“異常鏈”
在JDK1.4以前,程序員都必須自己編寫代碼來保持原始異常信息。從JDK 1.4以后,所有Throwable的子類在構造器可以接受一個cause 對象作為參數。這個cause就用來表示原始異常,這樣可以把原始異常傳遞給新的異常,使得即使在當前位置創建並拋出新的異常,你也能通過該異常鏈追蹤到異常最初發生的位置。希望通過上面的SalException去追蹤到最原始的異常信息,則可以將該方法改為下面的形式:
public void calSal() throws SalException
{
try
{
//實現結算工資的業務
...
}
catch (SQLException sqle)
{
//把原始異常記錄下來,留給管理員
...
//下面的sqle就是原始異常
throw new SalException(sqle);
}
catch (Exception e)
{
//把原始異常記錄下來,留給管理員
...
//下面的e就是原始異常
throw new SalException(e);
}
}
上面代碼拋出異常時,throw new SalException()傳入的參數是一個Exception異常,而不是傳入一個String異常,這就需要SalException類有相應的構造器。從JDK 1.4以后,Throwable基類已有一個可以接受Exception參數的方法,所以可以采用以下代碼來定義SalException.
public class SalException extends Exception
{
public SalException(){}
public SalException(String msg)
{
super(msg);
}
public SalException(Throwable t)
{
super(t);
}
創建這個SalException業務異常類后,就可以用它來封裝原始異常,從而實現對異常的鏈式處理。