異常和測試崗位的理念緊密相關,所以關於異常應該好好說一說,實際上異常機制是判斷一門編程語言是否成熟的標準,主流的編程語言中都提供了健全的異常處理機制,請看清楚這裏說的是處理機制,它可以使程序中的異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅的展現,而其本質是大大提高了程序的健壯性,反映到客戶層面更多的感知就是穩定性
對於計算機語言來說情況相當複雜,沒人能保證得了程序永遠不會出錯,就算程序沒有錯誤,誰又能保證客戶是按你的預期來輸入的?就算客戶是非常聰明而且配合的,誰能保證運行該程序會永久穩定?誰能保證運行程序的物理資源永久配合?誰能保證網絡條件永遠合適?。。。太多無法保證的東西,人是否要覆蓋住?
程序員喜歡的永遠是解決問題以及開發帶來的創造快感,都不願意當個堵漏洞的工人,而這纔是漏洞的真正概念,而這些也是一個程序員是否成熟的標準
以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("莊生曉夢迷蝴蝶");
}
}
}