之前我們介紹的基本類型、類、接口、枚舉都是在表示和操作數據,操作的過程中可能有很多出錯的情況,出錯的原因可能是多方面的,有的是不可控的內部原因,比如內存不夠了、磁盤滿了,有的是不可控的外部原因,比如網絡連接有問題,更多的可能是程序的編程錯誤,比如引用變量未初始化就直接調用實例方法。
這些非正常情況在Java中統一被認為是異常,Java使用異常機制來統一處理,由於內容較多,我們分為兩節來介紹,本節介紹異常的初步概念,以及異常類本身,下節主要介紹異常的處理。
我們先來通過一些例子認識一下異常。
初始異常
NullPointerException (空指針異常)
我們來看段代碼:
public class ExceptionTest { public static void main(String[] args) { String s = null; s.indexOf("a"); System.out.println("end"); } }
變量s沒有初始化就調用其實例方法indexOf,運行,屏幕輸出為:
Exception in thread "main" java.lang.NullPointerException at ExceptionTest.main(ExceptionTest.java:5)
輸出是告訴我們:在ExceptionTest類的main函數中,代碼第5行,出現了空指針異常(java.lang.NullPointerException)。
但,具體發生了什麼呢?當執行s.indexOf("a")的時候,Java系統發現s的值為null,沒有辦法繼續執行了,這時就啟用異常處理機制,首先創建一個異常對象,這裡是類NullPointerException的對象,然後查找看誰能處理這個異常,在示例代碼中,沒有代碼能處理這個異常,Java就啟用默認處理機制,那就是打印異常棧信息到屏幕,並退出程序。
在介紹函數調用原理的時候,我們介紹過棧,異常棧信息就包括了從異常發生點到最上層調用者的軌跡,還包括行號,可以說,這個棧信息是分析異常最為重要的信息。
Java的默認異常處理機制是退出程序,異常發生點後的代碼都不會執行,所以示例代碼中最後一行System.out.println("end")不會執行。
NumberFormatException (數字格式異常)
我們再來看一個例子,代碼如下:
public class ExceptionTest { public static void main(String[] args) { if(args.length<1){ System.out.println("請輸入數字"); return; } int num = Integer.parseInt(args[0]); System.out.println(num); } }
args表示命令行參數,這段代碼要求參數為一個數字,它通過Integer.parseInt將參數轉換為一個整數,並輸出這個整數。參數是用戶輸入的,我們沒有辦法強制用戶輸入什麼,如果用戶輸的是數字,比如123,屏幕會輸出123,但如果用戶輸的不是數字,比如abc,屏幕會輸出:
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:492) at java.lang.Integer.parseInt(Integer.java:527) at ExceptionTest.main(ExceptionTest.java:7)
出現了異常NumberFormatException。這個異常是怎麼產生的呢?根據異常棧信息,我們看相關代碼:
這是NumberFormatException類65行附近代碼:
64 static NumberFormatException forInputString(String s) { 65 return new NumberFormatException("For input string: \"" + s + "\""); 66 }
這是Integer類492行附近代碼:
490 digit = Character.digit(s.charAt(i++),radix); 491 if (digit < 0) { 492 throw NumberFormatException.forInputString(s); 493 } 494 if (result < multmin) { 495 throw NumberFormatException.forInputString(s); 496 }
將這兩處合為一行,主要代碼就是:
throw new NumberFormatException(...)
new NumberFormatException(...)是我們容易理解的,就是創建了一個類的對象,只是這個類是一個異常類。throw是什麼意思呢?就是拋出異常,它會觸發Java的異常處理機制。在之前的空指針異常中,我們沒有看到throw的代碼,可以認為throw是由Java虛擬機自己實現的。
throw關鍵字可以與return關鍵字進行對比,return代表正常退出,throw代表異常退出,return的返回位置是確定的,就是上一級調用者,而throw後執行哪行代碼則經常是不確定的,由異常處理機制動態確定。
異常處理機制會從當前函數開始查找看誰"捕獲"了這個異常,當前函數沒有就查看上一層,直到主函數,如果主函數也沒有,就使用默認機制,即輸出異常棧信息並退出,這正是我們在屏幕輸出中看到的。
對於屏幕輸出中的異常棧信息,程序員是可以理解的,但普通用戶無法理解,也不知道該怎麼辦,我們需要給用戶一個更為友好的信息,告訴用戶,他應該輸入的是數字,要做到這一點,我們需要自己"捕獲"異常。
"捕獲"是指使用try/catch關鍵字,我們看捕獲異常後的示例代碼:
public class ExceptionTest { public static void main(String[] args) { if(args.length<1){ System.out.println("請輸入數字"); return; } try{ int num = Integer.parseInt(args[0]); System.out.println(num); }catch(NumberFormatException e){ System.err.println("參數"+args[0] +"不是有效的數字,請輸入數字"); } } }
我們使用try/catch捕獲並處理了異常,try後面的大括號{}內包含可能拋出異常的代碼,括號後的catch語句包含能捕獲的異常和處理代碼,catch後面括號內是異常信息,包括異常類型和變量名,這裡是NumberFormatException e,通過它可以獲取更多異常信息,大括號{}內是處理代碼,這裡輸出了一個更為友好的提示信息。
捕獲異常後,程序就不會異常退出了,但try語句內異常點之後的其他代碼就不會執行了,執行完catch內的語句後,程序會繼續執行catch大括號外的代碼。
這樣,我們就對異常有了一個初步的了解,異常是相對於return的一種退出機制,可以由系統觸發,也可以由程序通過throw語句觸發,異常可以通過try/catch語句進行捕獲並處理,如果沒有捕獲,則會導致程序退出並輸出異常棧信息。異常有不同的類型,接下來,我們來認識一下。
異常類
Throwable
NullPointerException和NumberFormatException都是異常類,所有異常類都有一個共同的父類Throwable,它有4個public構造方法:
有兩個主要參數,一個是message,表示異常消息,另一個是cause,表示觸發該異常的其他異常。異常可以形成一個異常鏈,上層的異常由底層異常觸發,cause表示底層異常。
Throwable還有一個public方法用於設置cause:
Throwable initCause(Throwable cause)
Throwable的某些子類沒有帶cause參數的構造方法,就可以通過這個方法來設置,這個方法最多只能被調用一次。
所有構造方法中都有一句重要的函數調用:
fillInStackTrace();
它會將異常棧信息保存下來,這是我們能看到異常棧的關鍵。
Throwable有一些常用方法用於獲取異常信息:
void printStackTrace()
打印異常棧信息到標准錯誤輸出流,它還有兩個重載的方法:
void printStackTrace(PrintStream s) void printStackTrace(PrintWriter s)
打印棧信息到指定的流,關於PrintStream和PrintWriter我們後續文章介紹。
String getMessage() Throwable getCause()
獲取設置的異常message和cause
StackTraceElement[] getStackTrace()
獲取異常棧每一層的信息,每個StackTraceElement包括文件名、類名、函數名、行號等信息。
異常類體系
以Throwable為根,Java API中定義了非常多的異常類,表示各種類型的異常,部分類示意如下:
Throwable是所有異常的基類,它有兩個子類Error和Exception。
Error表示系統錯誤或資源耗盡,由Java系統自己使用,應用程序不應拋出和處理,比如圖中列出的虛擬機錯誤(VirtualMacheError)及其子類內存溢出錯誤(OutOfMemoryError)和棧溢出錯誤(StackOverflowError)。
Exception表示應用程序錯誤,它有很多子類,應用程序也可以通過繼承Exception或其子類創建自定義異常,圖中列出了三個直接子類:IOException(輸入輸出I/O異常),SQLException(數據庫SQL異常),RuntimeException(運行時異常)。
RuntimeException(運行時異常)比較特殊,它的名字有點誤導,因為其他異常也是運行時產生的,它表示的實際含義是unchecked exception (未受檢異常),相對而言,Exception的其他子類和Exception自身則是checked exception (受檢異常),Error及其子類也是unchecked exception。
checked還是unchecked,區別在於Java如何處理這兩種異常,對於checked異常,Java會強制要求程序員進行處理,否則會有編譯錯誤,而對於unchecked異常則沒有這個要求。下節我們會進一步解釋。
RuntimeException也有很多子類,下表列出了其中常見的一些:
異常 說明 NullPointerException 空指針異常 IllegalStateException 非法狀態 ClassCastException 非法強制類型轉換 IllegalArgumentException 參數錯誤 NumberFormatException 數字格式錯誤 IndexOutOfBoundsException 索引越界 ArrayIndexOutOfBoundsException 數組索引越界 StringIndexOutOfBoundsException 字符串索引越界這麼多不同的異常類其實並沒有比Throwable這個基類多多少屬性和方法,大部分類在繼承父類後只是定義了幾個構造方法,這些構造方法也只是調用了父類的構造方法,並沒有額外的操作。
那為什麼定義這麼多不同的類呢?主要是為了名字不同,異常類的名字本身就代表了異常的關鍵信息,無論是拋出還是捕獲異常時,使用合適的名字都有助於代碼的可讀性和可維護性。
自定義異常
除了Java API中定義的異常類,我們也可以自己定義異常類,一般通過繼承Exception或者它的某個子類,如果父類是RuntimeException或它的某個子類,則自定義異常也是unchecked exception,如果是Exception或Exception的其他子類,則自定義異常是checked exception。
我們通過繼承Exception來定義一個異常,代碼如下:
public class AppException extends Exception { public AppException() { super(); } public AppException(String message, Throwable cause) { super(message, cause); } public AppException(String message) { super(message); } public AppException(Throwable cause) { super(cause); } }
和很多其他異常類一樣,我們沒有定義額外的屬性和代碼,只是繼承了Exception,定義了構造方法並調用了父類的構造方法。
小結
本節,我們通過兩個例子對異常做了基本介紹,介紹了try/catch和throw關鍵字及其含義,同時介紹了Throwable以及以它為根的異常類體系。
下一節,讓我們進一步探討異常。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。
-----------
更多好評原創文章
計算機程序的思維邏輯 (1) - 數據和變量
計算機程序的思維邏輯 (5) - 小數計算為什麼會出錯?
計算機程序的思維邏輯 (6) - 如何從亂碼中恢復 (上)?
計算機程序的思維邏輯 (8) - char的真正含義
計算機程序的思維邏輯 (12) - 函數調用的基本原理
計算機程序的思維邏輯 (17) - 繼承實現的基本原理
計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍
計算機程序的思維邏輯 (19) - 接口的本質
計算機程序的思維邏輯 (20) - 為什麼要有抽象類?
計算機程序的思維邏輯 (21) - 內部類的本質
計算機程序的思維邏輯 (23) - 枚舉的本質