Java開發筆記(七十五)異常的處理:扔出與捕捉

前面介紹的幾種異常(不包含錯誤),編碼的時候沒認真看還發現不了,直到程序運行到特定的代碼跑不下去了,程序員纔會恍然大悟:原來這裏的代碼邏輯有問題。像這些在運行的時候才暴露出來的異常,又被稱做「運行時異常」,與之相對的另外一類異常叫作「非運行時異常」。所謂非運行時異常,指的是在編碼階段就被編譯器發現這裏存在潛在的風險,須要開發者關注並加以處理。好比把某個字符串轉換成日期類型,用到了SimpleDateFormat實例的parse方法,假若按照常規方式編碼,則編譯器會在parse這行提示代碼錯誤,並給出以下圖所示的處理建議小窗。html

可見編譯器提供了兩種解決辦法,第一種是「Add throws declaration」,表示要添加throws聲明;第二種是「Surround with try/catch」,表示要用try/catch語句把parse行包圍起來。爲了消除編譯錯誤,姑且先採用第一種解決方式,給parse行所在的方法添加「 throws ParseException」,下面是修改後的演示代碼:java

	// 解析異常:指定日期不是真實的日子
	// ParseException屬於編譯時異常,在編碼時就要處理,不然沒法編譯經過。
	// 處理方式有兩種:一種是往外丟異常,另外一種是經過try...catch...語句捕捉異常
	private static void getDateFromFormat() throws ParseException {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		String strDate = "2021/02/28";
		Date date = sdf.parse(strDate);
	}

 

然而不光是上面的getDateFromFormat方法須要添加throws聲明,連該方法所在的main方法也要添加throws聲明才行。好不容易把該加的throws語句全都加了,接着故意填個格式錯誤的日期字符串,運行這個格式轉換代碼,果真程序輸出了異常信息「java.text.ParseException: Unparseable date: "2021/02/28"」。
不過手工添加throws實在麻煩,得從調用parse的地方開始一層一層往上加過去,改動量太大。那麼再試試編譯器提供的第二種解決方式,也就是parse這行增長try/catch語句塊,具體代碼示例以下:程序員

	// 經過try...catch...語句捕捉日期的解析異常
	private static void getDateWithCatch() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		String strDate = "2021/02/28";
		try { // 開始當心翼翼地嘗試,隨時準備捕捉異常
			Date date = sdf.parse(strDate);
		} catch (ParseException e) { // 捕捉到了解析異常
			e.printStackTrace(); // 打印出錯時的棧軌跡信息
		}
	}

 

運行包含try/catch的以上代碼,程序依然打印ParseException的相關異常日誌,只是此時的打印動做由catch內部的「e.printStackTrace();」觸發。但這不是重點,重點在於try與catch兩個代碼塊之間的關係。從示例代碼可知,try後面放的是普通代碼,而catch後面放的是異常信息打印語句,它們對應着兩個分支:一個try正常分支,一個catch異常分支。若是try的內部代碼徹底正常運行,則異常分支的內部代碼根本不會執行;若是try的內部代碼運行出錯,則程序略過try的剩餘代碼,直接跳到異常分支處理。照這麼看,try/catch的處理邏輯相似於if/else,都存在「若是……就……,不然……」的分支操做。不一樣之處在於,try語句並不指定知足進入的條件,而是由程序在運行時根據是否發生異常來決定繼續處理仍是跳到異常分支。何況也不是全部的異常都能跳進catch分支,只有符合catch語句指定的異常種類,才能跳的進去,不然仍是往上一層一層扔出異常了。
有了try和catch這對好搭檔,程序運行時不論是正常分支仍是異常分支都可妥善處理了。不過有的業務須要在操做開始前分配資源,在操做結束後釋放資源,例如訪問數據庫就得先創建數據庫鏈接,再進行記錄的增刪改查等操做,最後處理完了再釋放數據庫鏈接。對於這種業務,不管是正常流程仍是異常流程,最終都得執行資源釋放操做。或許有人說,在try/catch整塊代碼後面補充釋放資源不就得了?要是針對if/else的業務場景,卻是能夠這麼幹;但如今業務場景變成try/catch,就不能如此蠻幹了。由於在try/catch整塊後面添加代碼,新代碼本質上仍走正常流程,即try/catch兩個分支並流以後的正常流程。同時catch語句只能捕捉到某種類型的異常,並不能捕捉到全部異常,也就是說,一旦try內部遇到了未知異常,這個未知異常不會跳到現有的catch分支(因catch分支沒法識別未知異常),而是當場一層一層往外扔出未知異常。這樣一來,跟在try/catch後面的資源釋放代碼根本沒機會執行,故而該方式將在遇到未知異常時失效。
爲了保證在全部狀況下(沒有異常,或者遇到任何一種異常包括未知異常)都能執行某段代碼,Java給try/catch機制增長了finally語句,該語句要求程序無論發生任何狀況都得進來到此一遊,像資源釋放這種代碼就適合放在finally內部,管你沒異常仍是有異常仍是什麼未知異常,最終通通拉到finally語句裏面走一遭。仍以日期轉換爲例,要求給某個字符串形式的日期加上若干天,若是字符串日期解析失敗,則自動用當前日期代替,而且不管遇到什麼異常,務必返回一個正常的日期字符串。據此聯合運用try/catch/finally,編寫出來的處理代碼以下所示:數據庫

	// 給指定日期加上若干天。若是日期解析失敗,則自動用當前日期代替
	private static String addSomeDays(String strDate, int number) {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		Date date = null;
		try { // 開始當心翼翼地嘗試,隨時準備捕捉異常
			date = sdf.parse(strDate);
		} catch (ParseException e) { // 捕捉到了解析異常
			date = new Date();
		} finally { // 不管是否發生異常,都要執行最終的代碼塊
			if (date == null) {
				date = new Date();
			}
			long time = date.getTime() + number*24*60*60*1000;
			date.setTime(time);
		}
		return sdf.format(date);
	}

 

這下總算實現了任意狀況都可正常運行的需求,try/catch/finally三兄弟聯手,正應了那句老話「三個臭皮匠,勝過諸葛亮」。數組

除了系統自帶的各類異常,程序員也可本身定義新的異常,自定義異常很簡單,只需從Exception派生出子類,並編寫該類的構造方法便可。下面即是兩個自定義異常的代碼例子,第一個是數組爲空異常,定義代碼以下所示:測試

//定義一個數組爲空異常。異常類必須由Exception派生而來
public class ArrayIsNullException extends Exception {
	private static final long serialVersionUID = -1L;

	public ArrayIsNullException(String message) {
		super(message);
	}
}

第二個是數組越界異常,定義代碼以下所示:編碼

//定義一個數組越界異常。異常類必須由Exception派生而來
public class ArrayOutOfException extends Exception {
	private static final long serialVersionUID = -1L;

	public ArrayOutOfException(String message) {
		super(message);
	}
}

因爲這兩個是自定義的異常,不會被系統自動丟出來,所以須要由程序員在代碼中手工扔出自定義的異常。扔出異常的代碼格式爲「throw 某異常的實例;」,異常扔出以後,假若當前方法沒有捕捉異常,則該方法還得在入參列表以後添加語句「throws 以逗號分隔的異常列表」,表示本方法處理不了這些異常,請求上級方法幫忙處理。舉個根據下標獲取數組元素的例子,正常獲取指定下標的元素有兩個前提:其一數組不能爲空,其二下標不能超出數組範圍。若是發現目標數組爲空,就令代碼扔出數組爲空異常ArrayIsNullException;若是發現下標不在合法的位置,就令代碼扔出數組越界異常ArrayOutOfException。按此思路編寫的方法代碼示例以下:日誌

	// 根據下標獲取指定數組對應位置的元素
	private static int getItemByIndex(int[] array, int index) 
			throws ArrayIsNullException, ArrayOutOfException { // 同時扔出了多個異常
		if (array == null) { // 若是數組爲空
			// 就扔出數組爲空異常
			throw new ArrayIsNullException("這是個空數組");
		} else if (index<0 || index>=array.length) { // 若是下標超出了數組範圍
			// 就扔出數組越界異常
			throw new ArrayOutOfException("下標超出了數組範圍");
		}
		return array[index];
	}

 

特別注意上面的異常扔出操做用到了兩個關鍵字,一個是沒帶s的throw,另外一個是帶s尾巴的throws,它們之間的區別不只僅是調用位置不一樣,並且一次扔出的異常數量也不一樣,throw每次只能扔出一個異常,而throws容許一次性扔出多個異常。
另外,剛纔的getItemByIndex方法扔出了兩個異常,留待它的上級方法接手爛攤子。上級方法當然能夠沿用try/catch語句捕捉異常,不過此次面對的是兩個異常不是單個異常,這也好辦,既然有兩個異常就寫上兩個異常分支唄,兩個catch分支分別捕捉數組爲空異常和數組越界異常。如此一來,上級方法的異常捕捉代碼就變成下面這般:orm

	// 進行數組的下標訪問測試(數組爲空)
	private static void testArrayByIndexWithNull() {
		int[] array = null;
		try { // 開始當心翼翼地嘗試,隨時準備捕捉異常
			// 根據下標獲取指定數組對應位置的元素
			int item = getItemByIndex(array, 3); 
			System.out.println("item="+item);
		} catch (ArrayIsNullException e) { // 捕捉到了數組爲空異常
			e.printStackTrace();  // 打印出錯時的棧軌跡信息
		} catch (ArrayOutOfException e) { // 捕捉到了下標越界異常
			e.printStackTrace();  // 打印出錯時的棧軌跡信息
		}
	}

 

看起來,catch分支彷彿if/else語句裏的else分支,都支持有多路的條件分支。當多個else分支的處理代碼保持一致時,則容許經過或操做將它們合併爲一個else分支;同理,假如多個catch分支的異常處理沒有差異,也支持引入或操做將它們合併爲一個catch分支,具體寫法形如「catch (異常A | 異常B e)」。合併異常分支以後的異常處理代碼以下所示:htm

	// 進行數組的下標訪問測試(下標越界)
	private static void testArrayByIndexWithOut() {
		int[] array = {1, 2, 3};
		try { // 開始當心翼翼地嘗試,隨時準備捕捉異常
			// 根據下標獲取指定數組對應位置的元素
			int item = getItemByIndex(array, 3); 
			System.out.println("item="+item);
		} catch (ArrayIsNullException | ArrayOutOfException e) { // 捕捉到了數組爲空異常或下標越界異常
			e.printStackTrace();  // 打印出錯時的棧軌跡信息
		}
	}

 

由於ArrayIsNullException和ArrayOutOfException都是Exception的子類,因此「ArrayIsNullException | ArrayOutOfException」能夠被「Exception」所取代,進一步簡化後的方法代碼以下:

	// 進行數組的下標訪問測試(捕獲全部異常)
	private static void testArrayByIndexWithAny() {
		int[] array = null;
		try { // 開始當心翼翼地嘗試,隨時準備捕捉異常
			// 根據下標獲取指定數組對應位置的元素
			int item = getItemByIndex(array, 3); 
			System.out.println("item="+item);
		} catch (Exception e) { // 捕捉到了任何一種異常
			e.printStackTrace();  // 打印出錯時的棧軌跡信息
		}
	}

 

上述代碼裏的異常分支「catch (Exception e)」表示將捕捉任何屬於Exception類型的異常,這些異常包括Exception自身及其派生出來的全部子類,固然也包含前面自定義的ArrayIsNullException和ArrayOutOfException了。



更多Java技術文章參見《Java開發筆記(序)章節目錄

相關文章
相關標籤/搜索