Java面向對象系列[v1.0.0][異常處理機制]

異常和測試崗位的理念緊密相關,所以關於異常應該好好說一說,實際上異常機制是判斷一門編程語言是否成熟的標準,主流的編程語言中都提供了健全的異常處理機制,請看清楚這裏說的是處理機制,它可以使程序中的異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅的展現,而其本質是大大提高了程序的健壯性,反映到客戶層面更多的感知就是穩定性

對於計算機語言來說情況相當複雜,沒人能保證得了程序永遠不會出錯,就算程序沒有錯誤,誰又能保證客戶是按你的預期來輸入的?就算客戶是非常聰明而且配合的,誰能保證運行該程序會永久穩定?誰能保證運行程序的物理資源永久配合?誰能保證網絡條件永遠合適?。。。太多無法保證的東西,人是否要覆蓋住?

程序員喜歡的永遠是解決問題以及開發帶來的創造快感,都不願意當個堵漏洞的工人,而這纔是漏洞的真正概念,而這些也是一個程序員是否成熟的標準

以Java爲例,它的異常處理主要依賴於try、catch、finally、throw和throws五個關鍵字:

  • try代碼塊中放置可能引發異常的代碼,因此程序員判斷代碼是否會有異常情況,是否需要try代碼塊來處理就成了關鍵,try只是工具,關鍵在人;
  • catch代碼塊對應異常類型以及該類型的處理方式;
  • 多個catch代碼塊後可以跟一個finally代碼塊它與try代碼塊相呼應,主要用於回收try代碼塊裏打開的物理資源,而異常處理機制會保證finally代碼塊總會被執行;
  • throws關鍵字主要是用於方法簽名,聲明該方法可能拋出的異常,而throw用於拋出一個實際的異常

Java的異常分爲兩種,Checked異常和Runtime異常,Checked異常和Runtime異常,Checked都是可以在編譯階段被處理的異常,因此它強制程序處理所有的Checked異常Runtime異常則無需處理,程序員處理異常是一個繁瑣的事情,因此程序的健壯性在人而非try代碼塊

使用try…catch捕獲異常

try
{
	// 業務實現代碼
	...
}
catch (Exception e)
{
	// 處理異常的代碼塊
}

如果程序可以順利完成,那就一切正常,如果try塊裏的業務邏輯代碼出現異常,系統會自動生成一個異常對象,該異常對象會被提交給Java runtime環境,而這個過程就被稱爲拋出異常,當拋出異常發生的時候,Java會尋找能夠處理該異常對象的catch塊,如果找到合適的catch塊,則把該異常對象交給該catch塊處理,這個過程被稱爲捕獲異常,如果找不到捕獲異常的catch塊,則Java runtime環境終止,Java程序也會退出
無論程序是否出現在try代碼塊中,只要執行該代碼塊出現了異常,系統總會自動生成異常對象,如果不處理則程序直接退出

import java.io.*;

public class Gobang
{
	// 定義一個二維數組來充當棋盤
	private String[][] board;
	// 定義棋盤的大小
	private static int BOARD_SIZE = 15;
	public void initBoard()
	{
		// 初始化棋盤數組
		board = new String[BOARD_SIZE][BOARD_SIZE];
		// 把每個元素賦爲"╋",用於在控制檯畫出棋盤
		for (var i = 0; i < BOARD_SIZE; i++)
		{
			for (var j = 0; j < BOARD_SIZE; j++)
			{
				board[i][j] = "╋";
			}
		}
	}
	// 在控制檯輸出棋盤的方法
	public void printBoard()
	{
		// 打印每個數組元素
		for (var i = 0; i < BOARD_SIZE; i++)
		{
			for (var j = 0; j < BOARD_SIZE; j++)
			{
				// 打印數組元素後不換行
				System.out.print(board[i][j]);
			}
			// 每打印完一行數組元素後輸出一個換行符
			System.out.print("\n");
		}
	}
	public static void main(String[] args) throws Exception
	{
		var gb = new Gobang();
		gb.initBoard();
		gb.printBoard();
		// 這是用於獲取鍵盤輸入的方法
		var br = new BufferedReader(
			new InputStreamReader(System.in));
		String inputStr = null;
		// br.readLine():每當在鍵盤上輸入一行內容按回車,
		// 用戶剛剛輸入的內容將被br讀取到。
		while ((inputStr = br.readLine()) != null)
		{
			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("╋"))
				{
					System.out.println("您輸入的座標點已有棋子了,"
						+ "請重新輸入");
					continue;
				}
				gb.board[xPos - 1][yPos - 1] = "●";
			}
			catch (Exception e)
			{
				System.out.println("您輸入的座標不合法,請重新輸入,"
					+ "下棋座標應以x,y的格式");
				continue;
			}

			gb.printBoard();
			System.out.println("請輸入您下棋的座標,應以x,y的格式:");
		}
	}
}

程序中catch代碼塊處理了異常後,使用continue忽略本次循環剩下的代碼,開始執行下一次循環,這就保證了足夠的兼容性,用戶可以隨意輸入,程序不會因爲用戶輸入不合法而突然退出

查找catch代碼塊

當Java runtime環境接收到異常對象的時候,會依次判斷該異常對象是否是catch塊裏的異常類或者其子類的實例,如果是Java runtime將調用該catch塊來處理該異常,否則再次拿該異常對象與下一個catch塊裏的異常類進行比較,如下圖所示
在這裏插入圖片描述

Java異常類的繼承體系

如圖所示,Java把所有的非正常情況非爲兩種即:異常(Exception)和錯誤(Error),他們都繼承Throwable父類
Error:一般是指與虛擬機相關的問題,如系統崩潰、虛擬機錯誤、動態鏈接失敗等,這種錯誤無法恢復或不可能捕獲,將導致程序中斷,一般情況下應用程序無法處理這些錯誤,因此應用程序不應該試圖使用catch塊來捕獲Error對象,在定義該方法時,無須在其throws子句中聲明該方法可能拋出Error及其任何子類
在這裏插入圖片描述

public class DivTest
{
	public static void main(String[] args)
	{
		try
		{
			var a = Integer.parseInt(args[0]);
			var b = Integer.parseInt(args[1]);
			var c = a / b;
			System.out.println("您輸入的兩個數相除的結果是:" + c );
		}
		catch (IndexOutOfBoundsException ie)
		{
			System.out.println("數組越界:運行程序時輸入的參數個數不夠");
		}
		catch (NumberFormatException ne)
		{
			System.out.println("數字格式異常:程序只能接受整數參數");
		}
		catch (ArithmeticException ae)
		{
			System.out.println("算術異常");
		}
		catch (Exception e)
		{
			System.out.println("未知異常");
		}
	}
}

上面程序針對IndexOutOfBoundsException、NumberFormatException、ArithmeticException類型的異常,提供了專門的異常處理邏輯。
Java運行時的異常處理邏輯可能有如下幾種情形:

  • 如果運行該程序時輸入的參數不夠,將會發生數組越界異常,Java運行時將調用IndexOutOfBoundsException對應的catch塊處理該異常
  • 如果運行該程序時輸入的參數不是數字,而是字母,將發生數字格式異常, Java運行時將會調用NumberFormatException對應的catch塊處理該異常
  • 如果運行該程序時輸入的第二個參數是0,將會發生除0異常,Java運行時將會調用ArithmeticException對應的catch塊處理該異常
  • 如果程序運行時出現其他異常,該異常對象總是Exception類或其子類的實例,Java運行時將調用Exception對象的catch塊處理該異常
import java.util.*;

public class NullTest
{
	public static void main(String[] args)
	{
		Date d = null;
		try
		{
			System.out.println(d.after(new Date()));
		}
		catch (NullPointerException ne)
		{
			System.out.println("空指針異常");
		}
		catch (Exception e)
		{
			System.out.println("未知異常");
		}
	}
}

當試圖調用一個null對象的實例方法或實例變量的時,就會引發NullPointerException異常,Java運行時會調用NullPointerException對應的catch塊來處理該異常,如果遇到其他異常則調用最後的catch塊來處理異常

注意:Exception類的catch塊必須方法最後,因爲所有的異常對象都是Exception或其子類的實例,如果Exception類的catch塊在前邊,那麼它後邊的catch塊將永遠不會獲得執行

實際上進行異常捕獲的時候不僅應該把Exception類對應的catch塊放在最後,而且所有父類異常的catch塊都應該排在子類異常catch塊後面,也就是先處理小異常再處理大異常,否則將出現編譯錯誤

try
{
	statements...
}
catch (RuntimeException e)
{
	System.out.println("運行時異常");
}
catch (NullPointerException ne)
{
	System.out.println("空指針異常");
}

因爲RuntimeException已經包括了NullPointerException異常,所以catch (NullPointerException ne)處的catch塊永遠不會獲得執行的機會

多異常捕獲

在Java7之後,一個catch塊可以捕獲多種類型的異常,只需要在多種異常類型之間使用豎線(|)隔開,並且異常變量有隱式的final修飾,因此程序不能對異常變量重新賦值

public class MultiExceptionTest
{
	public static void main(String[] args)
	{
		try
		{
			var a = Integer.parseInt(args[0]);
			var b = Integer.parseInt(args[1]);
			var c = a / b;
			System.out.println("您輸入的兩個數相除的結果是:" + c );
		}
		catch (IndexOutOfBoundsException|NumberFormatException
			|ArithmeticException ie)
		{
			System.out.println("程序發生了數組越界、數字格式異常、算術異常之一");
			// 捕捉多異常時,異常變量默認有final修飾,
			// 所以下面代碼有錯:
			ie = new ArithmeticException("test");  
		}
		catch (Exception e)
		{
			System.out.println("未知異常");
			// 捕捉一個類型的異常時,異常變量沒有final修飾
			// 所以下面代碼完全正確。
			e = new RuntimeException("test");    
		}
	}
}

訪問異常類信息

如果程序需要在catch塊中訪問異常對象的相關信息,可以通過訪問catch塊的後異常形參來獲得,當Java運行時決定調用某個catch塊來處理該異常對象時,會將異常對象賦給catch塊後的異常參數,程序即可通過該參數來獲得異常的相關信息
所有的異常對象都包含了如下幾個常用方法:

  • getMessage():返回該異常的詳細描述字符串
  • printStackTrace():將該異常的跟蹤棧信息輸出到標準錯誤輸出
  • printStackTrace(PrintStream s): 將該異常的跟蹤棧信息輸出到指定輸出流
  • getStackTrace():返回該異常的跟蹤棧信息
import java.io.*;

public class AccessExceptionMsg
{
	public static void main(String[] args)
	{
		try
		{
			var fis = new FileInputStream("a.txt");
		}
		catch (IOException ioe)
		{
			System.out.println(ioe.getMessage());
			ioe.printStackTrace();
		}
	}
}

finally塊的作用

Java的垃圾回收機制不會回收任何物理資源,垃圾回收機制只能回收堆內存中對象所佔用的內存,而程序在try代碼塊裏打開的物理資源如數據庫連接、網絡資源、磁盤文件等,這些資源都必須顯示的回收,而這些顯示的回收應該在finally塊中做,因爲無論try和catch執行的什麼,finally代碼塊總會被執行

try
{
	// 業務代碼
	...
}
catch (SubException e)
{
	// 異常處理代碼塊
}
catch (SubException e)
{
	// 異常處理代碼塊
}
...
finally
{
	// 資源回收代碼塊
	...
}
  • 異常處理機制中必須有try代碼塊,catch和finally都是可選的,但catch和finally必須有其一,也可以同時有
  • 可以有多個catch塊,捕獲父類異常的catch塊必須位於捕獲子類異常的後面
  • 不能只有try代碼塊,既沒有catch也沒有finally
  • 多個catch代碼塊必須位於try代碼塊之後,finally塊必須位於所有的catch塊之後
import java.io.*;

public class FinallyTest
{
	public static void main(String[] args)
	{
		FileInputStream fis = null;
		try
		{
			fis = new FileInputStream("a.txt");
		}
		catch (IOException ioe)
		{
			System.out.println(ioe.getMessage());
			// return語句強制方法返回
			return;       
			// 使用exit來退出虛擬機
			System.exit(1);    
		}
		finally
		{
			// 關閉磁盤文件,回收資源
			if (fis != null)
			{
				try
				{
					fis.close();
				}
				catch (IOException ioe)
				{
					ioe.printStackTrace();
				}
			}
			System.out.println("執行finally塊裏的資源回收!");
		}
	}
}
  • 即便有return強制方法返回,但仍舊會執行finally代碼塊裏的代碼
  • System.exit(1)強制退出JVM,這種退出的話finally就沒機會執行了
  • 如果在finally塊中使用了return或throw語句,將會導致try代碼塊、catch代碼塊中的return、throw語句失效
public class FinallyFlowTest
{
	public static void main(String[] args)
		throws Exception
	{
		boolean a = test();
		System.out.println(a);
	}
	public static boolean test()
	{
		try
		{
			// 因爲finally塊中包含了return語句
			// 所以下面的return語句失去作用
			return true;
		}
		finally
		{
			return false;
		}
	}
}

當系統遇到try和catch裏的return或者throw語句的時候,都會立即終止執行當前方法,當方法執行並未結束,且return和throw語句也未執行,然後程序去尋找finally代碼塊,如果沒有finally代碼塊程序立即執行return或throw語句方法終止,如果有finally代碼塊,系統立即執行finally代碼塊,當finally代碼塊執行完畢後系統纔會跳回去執行try代碼塊、catch代碼塊裏的return或throw語句,如果finally裏也使用了return或throw等導致方法終止的語句,finally代碼塊就終止了系統也不會跳回去執行try代碼塊、catch代碼塊裏的任何代碼了

自動關閉資源的try語句

當程序使用finally代碼塊關閉資源時,顯得非常臃腫,Java7之後允許try關鍵字後跟一對圓括號用於聲明、初始化一個或多個資源,然後try語句在該語句結束時自動關閉這些資源,從而降低了代碼的臃腫
需要說明的是,要保證try語句可以正常關閉資源,這些資源實現類必須實現AutoCloseable或Closeable接口,實現這兩個接口就必須實現close()方法

  • Closeable是AutoCloseable的子接口,可以被自動關閉的資源類要麼實現AutoCloseable接口,要麼實現Closeable接口
  • Closeable接口裏的close()方法聲明拋出了IOException,因此它的實現類在實現close()方法時只能聲明拋出IOException或其子類
  • AutoCloseable接口裏的close()方法聲明拋出了Exception,因此它的實現類在實現close()方法時可以聲明拋出任何異常
import java.io.*;

public class AutoCloseTest
{
	public static void main(String[] args)
		throws IOException
	{
		try (
			// 聲明、初始化兩個可關閉的資源
			// try語句會自動關閉這兩個資源。
			var br = new BufferedReader(
				new FileReader("AutoCloseTest.java"));
			var ps = new PrintStream(new
				FileOutputStream("a.txt")))
		{
			// 使用兩個資源
			System.out.println(br.readLine());
			ps.println("莊生曉夢迷蝴蝶");
		}
	}
}

自動關閉資源的try語句相當於包含了隱式的finally代碼塊,因此這個代碼既沒有catch也沒有finally,Java7之後幾乎所有的資源類,包括文件IO的各種類、JDBC的Connection、Statement等接口進行了改寫,改寫後資源類都實現了AutoCloseable或Closeable接口

Java9之後不要求在try後的圓括號內聲明並創建資源,只需要自動關閉的資源有final修飾或者是有效的final,在Java9之後改寫上面的代碼

import java.io.*;

public class AutoCloseTest2
{
	public static void main(String[] args)
		throws IOException
	{
		// 有final修飾的資源
		final var br = new BufferedReader(
			new FileReader("AutoCloseTest.java"));
		// 沒有顯式使用final修飾,但只要不對該變量重新賦值,按該變量就是有效的final
		var ps = new PrintStream(new
			FileOutputStream("a.txt"));
		// 只要將兩個資源放在try後的圓括號內即可
		try (br; ps)
		{
			// 使用兩個資源
			System.out.println(br.readLine());
			ps.println("莊生曉夢迷蝴蝶");
		}
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章