9.3 使用throw拋出異常



當程序出現錯誤,系統會自動拋出異常;除此之外,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業務異常類后,就可以用它來封裝原始異常,從而實現對異常的鏈式處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM