異常的概念與體系結(jié)構(gòu)
異常的概念
在生活中,一個(gè)人表情痛苦,出于關(guān)心,可能會(huì)問:你是不是生病了,需要我陪你去看醫(yī)生嗎?
在程序中也是一樣,程序猿是一幫辦事嚴(yán)謹(jǐn)、追求完美的高科技人才。在日常開發(fā)中,絞盡腦汁將代碼寫的盡善盡美,在程序運(yùn)行過程中,難免會(huì)出現(xiàn)一些奇奇怪怪的問題。
有時(shí)通過代碼很難去控制,比如:數(shù)據(jù)格式不對(duì)、網(wǎng)絡(luò)不通暢、內(nèi)存報(bào)警等
在Java中,將程序執(zhí)行過程中發(fā)生的不正常行為稱為異常
下來我們先來簡(jiǎn)單看幾個(gè)異常
算術(shù)異常
如以下代碼
System.out.println(21 / 0);
運(yùn)行時(shí)就會(huì)出現(xiàn)
數(shù)組越界異常
如以下代碼
int[] arr = {1, 2, 3,4,5};
System.out.println(arr[10]);
運(yùn)行時(shí)就會(huì)出現(xiàn)
空指針異常
如以下代碼
int[] arr = null;
System.out.println(arr.length);
運(yùn)行時(shí)就會(huì)出現(xiàn)
從上述過程中可以看到,java中不同類型的異常,都有與其對(duì)應(yīng)的類來進(jìn)行描述
注意:
語法錯(cuò)誤不是異常
異常的體系結(jié)構(gòu)
異常種類繁多,為了對(duì)不同異?;蛘咤e(cuò)誤進(jìn)行很好的分類管理,Java內(nèi)部維護(hù)了一個(gè)異常的體系結(jié)構(gòu):
從上圖中可以看到:
- Throwable:是異常體系的頂層類,其派生出兩個(gè)重要的子類, Error 和 Exception
- Error:指的是Java虛擬機(jī)無法解決的嚴(yán)重問題,比如:JVM的內(nèi)部錯(cuò)誤、資源耗盡等,典型代表:StackOverflowError和OutOfMemoryError,一旦發(fā)生回力乏術(shù)。
- Exception:異常產(chǎn)生后程序員可以通過代碼進(jìn)行處理,使程序繼續(xù)執(zhí)行。比如:感冒、發(fā)燒。我們平時(shí)所說的異常就是Exception。
異常的分類
異常可能在編譯時(shí)發(fā)生,也可能在程序運(yùn)行時(shí)發(fā)生,根據(jù)發(fā)生的時(shí)機(jī)不同,可以將異常分為:運(yùn)行時(shí)異常和編譯時(shí)異常
編譯時(shí)異常
在程序編譯期間發(fā)生的異常,稱為編譯時(shí)異常,也稱為受檢查異常(Checked Exception)
比如博主在接口時(shí)講的覆寫Object類的clone
編譯時(shí)報(bào)錯(cuò): Error:(17, 35) java:
未報(bào)告的異常錯(cuò)誤java.lang.CloneNotSupportedException; 必須對(duì)其進(jìn)行捕獲或聲明以便拋出
運(yùn)行時(shí)異常
在程序執(zhí)行期間發(fā)生的異常,稱為運(yùn)行時(shí)異常,也稱為非受檢查異常(Unchecked Exception)
RunTimeException以及其子類對(duì)應(yīng)的異常,都稱為運(yùn)行時(shí)異常。比如:
NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。
注意:編譯時(shí)出現(xiàn)的語法性錯(cuò)誤,不能稱之為異常。例如將 System.out.println 拼寫錯(cuò)了, 寫成了system.out.println. 此時(shí)編譯過程中就會(huì)出錯(cuò), 這是 “編譯期” 出錯(cuò)。而運(yùn)行時(shí)指的是程序已經(jīng)編譯通過得到class 文件了, 再由 JVM 執(zhí)行過程中出現(xiàn)的錯(cuò)誤
異常的處理
異常處理的核心思想就是 EAFP。
在Java中,異常處理主要的5個(gè)關(guān)鍵字:throw、try、catch、final、throws。
后面會(huì)一一進(jìn)行介紹
防御式編程
錯(cuò)誤在代碼中是客觀存在的. 因此我們要讓程序出現(xiàn)問題的時(shí)候及時(shí)通知程序猿. 主要方式有:LBYL事前防御型和EAFP事后認(rèn)錯(cuò)型
LBYL:事前防御型
Look Before You Leap. 在操作之前就做充分的檢查. 即:事前防御型
比如我們?cè)诖蛴螒驎r(shí),會(huì)提示你一些東西,比如登錄游戲錯(cuò)誤、匹配錯(cuò)誤······
boolean ret = false;
ret = 登陸游戲();
if (!ret) {
登陸游戲錯(cuò)誤;
return;
}
ret = 開始匹配();
if (!ret) {
匹配錯(cuò)誤;
return;
}
ret = 游戲確認(rèn)();
if (!ret) {
游戲確認(rèn)錯(cuò)誤;
return;
這時(shí)候就會(huì)向程序員進(jìn)行報(bào)錯(cuò),并且不會(huì)向下進(jìn)行
但是也存在缺陷
缺陷:正常流程和錯(cuò)誤處理流程代碼混在一起, 代碼整體顯的比較混亂
EAFP:事后認(rèn)錯(cuò)型
It’s Easier to Ask Forgiveness than Permission. “事后獲取原諒比事前獲取許可更容易”. 也就是先操作, 遇到問題再處理. 即:事后認(rèn)錯(cuò)型
我們先運(yùn)行程序,當(dāng)程序遇到問題再再處理問題。比如
try {
登陸游戲();
開始匹配();
游戲確認(rèn)();
選擇英雄();
載入游戲畫面();
...
} catch (登陸游戲異常) {
處理登陸游戲異常;
} catch (開始匹配異常) {
處理開始匹配異常;
} catch (游戲確認(rèn)異常) {
處理游戲確認(rèn)異常;
} catch (選擇英雄異常) {
處理選擇英雄異常;
} catch (載入游戲畫面異常) {
處理載入游戲畫面異常;
}
...
這樣做的好處在于
優(yōu)勢(shì):正常流程和錯(cuò)誤流程是分離開的, 程序員更關(guān)注正常流程,代碼更清晰,容易理解代碼
異常的拋出
在編寫程序時(shí),如果程序中出現(xiàn)錯(cuò)誤,此時(shí)就需要將錯(cuò)誤的信息告知給調(diào)用者,比如:參數(shù)檢測(cè)。
在Java中,可以借助throw關(guān)鍵字,拋出一個(gè)指定的異常對(duì)象,將錯(cuò)誤信息告知給調(diào)用者。具體語法如下
throw new XXXException("異常產(chǎn)生的原因");
示例:實(shí)現(xiàn)一個(gè)獲取數(shù)組中任意位置元素的方法
public static int getElement(int[] array, int index){
if(null == array){
throw new NullPointerException("傳遞的數(shù)組為null");
} if(
index < 0 || index >= array.length){
throw new ArrayIndexOutOfBoundsException("傳遞的數(shù)組下標(biāo)越界");
}
return array[index];
}
public static void main(String[] args) {
int[] array = {1,2,3};
getElement(array, 3);
}
我們?cè)賮砜匆幌逻\(yùn)行結(jié)果
這里拋出了數(shù)組越界的異常
注意事項(xiàng)
- throw必須寫在方法體內(nèi)部
- 拋出的對(duì)象必須是Exception 或者 Exception 的子類對(duì)象
- 如果拋出的是 RunTimeException 或者 RunTimeException 的子類,則可以不用處理,直接交給JVM來處理
- 如果拋出的是編譯時(shí)異常,用戶必須處理,否則無法通過編譯
- 異常一旦拋出,其后的代碼就不會(huì)執(zhí)行
異常的捕獲
異常的捕獲,也就是異常的具體處理方式,主要有兩種:異常聲明throws 以及 try-catch捕獲處理
異常聲明throws
處在方法聲明時(shí)參數(shù)列表之后,當(dāng)方法中拋出編譯時(shí)異常,用戶不想處理該異常,此時(shí)就可以借助throws將異常拋給方法的調(diào)用者來處理。即當(dāng)前方法不處理異常,提醒方法的調(diào)用者處理異常。
語法格式
語法格式:
修飾符 返回值類型 方法名(參數(shù)列表) throws 異常類型1,異常類型2...{
}
比如我們前面提到的Cloneable時(shí)重寫clone()方法時(shí)
public class Person implements Cloneable{
private String name;
private String gender;
int age;
@Override
public Person clone() throws CloneNotSupportedException {
return (Person)super.clone();
}
}
再舉個(gè)例子
我們現(xiàn)在需要,加載指定的配置文件config.ini
當(dāng)我們書寫方法時(shí)此處不處理,也沒有能力處理,應(yīng)該將錯(cuò)誤信息報(bào)告給調(diào)用者,讓調(diào)用者檢查文件名字是否給錯(cuò)誤了。
public class Config {
File file;
/*
FileNotFoundException : 編譯時(shí)異常,表明文件不存在
此處不處理,也沒有能力處理,應(yīng)該將錯(cuò)誤信息報(bào)告給調(diào)用者,讓調(diào)用者檢查文件名字是否給錯(cuò)誤了
*/
public void OpenConfig(String filename) throws FileNotFoundException{
if(filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不對(duì)");
}
// 打開文件
}
}
注意事項(xiàng)
- throws必須跟在方法的參數(shù)列表之后
- 聲明的異常必須是 Exception 或者 Exception 的子類
- 方法內(nèi)部如果拋出了多個(gè)異常,throws之后必須跟多個(gè)異常類型,之間用逗號(hào)隔開,如果拋出多個(gè)異常類型具有父子關(guān)系,直接聲明父類即可。比如
File file;
// public void OpenConfig(String filename) throws IOException,FileNotFoundException{
// FileNotFoundException 繼承自 IOException
public void OpenConfig(String filename) throws IOException{
if(filename.endsWith(".ini")){
throw new IOException("文件不是.ini文件");
}
if(filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不對(duì)");
}
// 打開文件
}
- 調(diào)用聲明拋出異常的方法時(shí),調(diào)用者必須對(duì)該異常進(jìn)行處理,或者繼續(xù)使用throws拋出
try-catch捕獲并處理
throws對(duì)異常并沒有真正處理,而是將異常報(bào)告給拋出異常方法的調(diào)用者,由調(diào)用者處理。如果真正要對(duì)異常進(jìn)行處理,就需要try-catch。
語法格式
語法格式:
try{
// 將可能出現(xiàn)異常的代碼放在這里
}catch(要捕獲的異常類型 e){
// 如果try中的代碼拋出異常了,此處catch捕獲時(shí)異常類型與try中拋出的異常類型一致時(shí),或者是try中拋出異常的基類時(shí),就會(huì)被捕獲到
// 對(duì)異常就可以正常處理,處理完成后,跳出try-catch結(jié)構(gòu),繼續(xù)執(zhí)行后序代碼
}[catch(異常類型 e){
// 對(duì)異常進(jìn)行處理
}finally{
// 此處代碼一定會(huì)被執(zhí)行到
}]
// 后序代碼
// 當(dāng)異常被捕獲到時(shí),異常就被處理了,這里的后序代碼一定會(huì)執(zhí)行
// 如果捕獲了,由于捕獲時(shí)類型不對(duì),那就沒有捕獲到,這里的代碼就不會(huì)被執(zhí)行
注意:
- []中表示可選項(xiàng),可以添加,也可以不用添加
- try中的代碼可能會(huì)拋出異常,也可能不會(huì)
比如我現(xiàn)在有這樣一個(gè)需求:讀取配置文件,如果配置文件名字不是指定名字,拋出異常,調(diào)用者進(jìn)行異常處理
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Config {
File file;
public void openConfig(String filename) throws FileNotFoundException{
if(!filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不對(duì)");
}
// 打開文件
}
public void readConfig(){
}
public static void main(String[] args) {
Config config = new Config();
try {
config.openConfig("config.txt");
System.out.println("文件打開成功");
} catch (IOException e) {
// 異常的處理方式
System.out.println(e.getMessage()); // 只打印異常信息
System.out.println(e); // 打印異常類型:異常信息
e.printStackTrace(); // 打印信息最全面
}
// 一旦異常被捕獲處理了,此處的代碼會(huì)執(zhí)行
System.out.println("異常如果被處理了,這里的代碼也可以執(zhí)行");
}
}
我們看到異常的處理方式哪里有三個(gè)不同的處理方式,我們來看一下三種不同的處理方式的效果
如何選擇異常的處理方式
講了這么多異常的處理方式,那么我們實(shí)際中應(yīng)該選擇哪一種呢?
異常的種類有很多, 我們要根據(jù)不同的業(yè)務(wù)場(chǎng)景來決定.
對(duì)于比較嚴(yán)重的問題(例如和算錢相關(guān)的場(chǎng)景), 應(yīng)該讓程序直接崩潰, 防止造成更嚴(yán)重的后果
對(duì)于不太嚴(yán)重的問題(大多數(shù)場(chǎng)景), 可以記錄錯(cuò)誤日志, 并通過監(jiān)控報(bào)警程序及時(shí)通知程序猿
對(duì)于可能會(huì)恢復(fù)的問題(和網(wǎng)絡(luò)相關(guān)的場(chǎng)景), 可以嘗試進(jìn)行重試.
在我們當(dāng)前的代碼中采取的是經(jīng)過簡(jiǎn)化的第二種方式. 我們記錄的錯(cuò)誤日志是出現(xiàn)異常的方法調(diào)用信息, 能很
快速的讓我們找到出現(xiàn)異常的位置. 以后在實(shí)際工作中我們會(huì)采取更完備的方式來記錄異常信息
注意事項(xiàng)
- try塊內(nèi)拋出異常位置之后的代碼將不會(huì)被執(zhí)行
- 如果拋出異常類型與catch時(shí)異常類型不匹配,即異常不會(huì)被成功捕獲,也就不會(huì)被處理,繼續(xù)往外拋,直到JVM收到后中斷程序----異常是按照類型來捕獲的
- try中可能會(huì)拋出多個(gè)不同的異常對(duì)象,則必須用多個(gè)catch來捕獲----即多種異常,多次捕獲
如果多個(gè)異常的處理方式是完全相同, 也可以寫成這樣
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
...
}
如果異常之間具有父子關(guān)系,一定是子類異常在前catch,父類異常在后catch,否則語法錯(cuò)誤:
4. 可以通過一個(gè)catch捕獲所有的異常,即多個(gè)異常,一次捕獲(不推薦)
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("after try catch");
}
由于 Exception 類是所有異常類的父類. 因此可以用這個(gè)類型表示捕捉所有異常.
備注: catch 進(jìn)行類型匹配的時(shí)候, 不光會(huì)匹配相同類型的異常對(duì)象, 也會(huì)捕捉目標(biāo)異常類型的子類對(duì)象. 如剛才的代碼,
NullPointerException 和 ArrayIndexOutOfBoundsException 都是 Exception的子類, 因此都能被捕獲到.
finally
在寫程序時(shí),有些特定的代碼,不論程序是否發(fā)生異常,都需要執(zhí)行,比如程序中打開的資源:網(wǎng)絡(luò)連接、數(shù)據(jù)庫連接、IO流等,在程序正常或者異常退出時(shí),必須要對(duì)資源進(jìn)行回收。另外,因?yàn)楫惓?huì)引發(fā)程序的跳轉(zhuǎn),可能導(dǎo)致有些語句執(zhí)行不到,finally就是用來解決這個(gè)問題的。
語法格式:
語法格式:
try{
// 可能會(huì)發(fā)生異常的代碼
}catch(異常類型 e){
// 對(duì)捕獲到的異常進(jìn)行處理
}finally{
// 此處的語句無論是否發(fā)生異常,都會(huì)被執(zhí)行到
}
// 如果沒有拋出異常,或者異常被捕獲處理了,這里的代碼也會(huì)執(zhí)行
示例:
既然 finally 和 try-catch-finally 后的代碼都會(huì)執(zhí)行,那為什么還要有finally呢
比如我現(xiàn)在有這樣一個(gè)需求:實(shí)現(xiàn)getData方法,內(nèi)部輸入一個(gè)整形數(shù)字,然后將該數(shù)字返回,并在main方法中打印
上述程序,如果正常輸入,成功接收輸入后程序就返回了,try-catch-finally之后的代碼根本就沒有執(zhí)行,即輸入流就沒有被釋放,造成資源泄漏。
注意:finally中的代碼一定會(huì)執(zhí)行的,一般在finally中進(jìn)行一些資源清理的掃尾工作
比如下面這一道題:
public static void main(String[] args) {
System.out.println(func());
}
public static int func() {
try {
return 10;
} finally {
return 20;
}
}
A:10 B:20 C:30 D:編譯失敗
**答案:**20
解析:finally 執(zhí)行的時(shí)機(jī)是在方法返回之前(try 或者 catch 中如果有 return 會(huì)在這個(gè) return 之前執(zhí)行 finally). 但是如果finally 中也存在 return 語句, 那么就會(huì)執(zhí)行 finally 中的 return, 從而不會(huì)執(zhí)行到 try 中原有的 return.
一般我們不建議在 finally 中寫 return (被編譯器當(dāng)做一個(gè)警告)
異常捕獲相關(guān)面試題
- throw 和 throws 的區(qū)別?
(1)throws用于方法頭,表示的只是異常的申明,而throw用于方法內(nèi)部,拋出的是異常對(duì)象。
(2)throws可以一次性拋出多個(gè)異常,而throw只能一個(gè)
(3)throws拋出異常時(shí),它的上級(jí)(調(diào)用者)也要申明拋出異?;蛘卟东@,不然編譯報(bào)錯(cuò)。而throw的話,可以不申明或不捕獲(這是非常不負(fù)責(zé)任的方式)但編譯器不會(huì)報(bào)錯(cuò)。
- finally中的語句一定會(huì)執(zhí)行嗎?
一定很執(zhí)行
異常的處理流程
如果本方法中沒有合適的處理異常的方式, 就會(huì)沿著調(diào)用棧向上傳遞
關(guān)于調(diào)用棧,博主在C語言部分也講解過,感興趣的同學(xué)可以去下方鏈接進(jìn)行學(xué)習(xí)
函數(shù)棧幀的創(chuàng)建于銷毀
這里博主再進(jìn)行簡(jiǎn)單進(jìn)行介紹一下
關(guān)于 “調(diào)用?!?br> 方法之間是存在相互調(diào)用關(guān)系的, 這種調(diào)用關(guān)系我們可以用 “調(diào)用?!?來描述. 在 JVM 中有一塊內(nèi)存空間稱為"虛擬機(jī)棧" 專門存儲(chǔ)方法之間的調(diào)用關(guān)系. 當(dāng)代碼中出現(xiàn)異常的時(shí)候, 我們就可以使用 e.printStackTrace(); 的方式查看出現(xiàn)異常代碼的調(diào)用棧
例如
如果向上一直傳遞都沒有合適的方法處理異常, 最終就會(huì)交給 JVM 處理, 程序就會(huì)異常終止(和我們最開始未使用 try catch 時(shí)是一樣的).
可以看到, 程序已經(jīng)異常終止了, 沒有執(zhí)行到System.out.println("after try catch");
這一行
異常處理流程總結(jié)
- 程序先執(zhí)行 try 中的代碼
- 如果 try 中的代碼出現(xiàn)異常, 就會(huì)結(jié)束 try 中的代碼, 看和 catch 中的異常類型是否匹配.
- 如果找到匹配的異常類型, 就會(huì)執(zhí)行 catch 中的代碼
- 如果沒有找到匹配的異常類型, 就會(huì)將異常向上傳遞到上層調(diào)用者.
- 無論是否找到匹配的異常類型, finally 中的代碼都會(huì)被執(zhí)行到(在該方法結(jié)束之前執(zhí)行).
- 如果上層調(diào)用者也沒有處理的了異常, 就繼續(xù)向上傳遞.
- 一直到 main 方法也沒有合適的代碼處理異常, 就會(huì)交給 JVM 來進(jìn)行處理, 此時(shí)程序就會(huì)異常終止.
自定義異常類
Java 中雖然已經(jīng)內(nèi)置了豐富的異常類, 但是并不能完全表示實(shí)際開發(fā)中所遇到的一些異常,此時(shí)就需要維護(hù)符合我們實(shí)際情況的異常結(jié)構(gòu)
例如:我們實(shí)現(xiàn)一個(gè)用戶登陸功能
public class LogIn {
private String userName = "admin";
private String password = "123456";
public static void loginInfo(String userName, String password) {
if (!userName.equals(userName)) {
}
if (!password.equals(password)) {
}
System.out.println("登陸成功");
}
public static void main(String[] args) {
loginInfo("admin", "123456");
}
}
此時(shí)我們?cè)谔幚碛脩裘艽a錯(cuò)誤的時(shí)候可能就需要拋出兩種異常. 我們可以基于已有的異常類進(jìn)行擴(kuò)展(繼承), 創(chuàng)建和我們業(yè)務(wù)相關(guān)的異常類.
具體方式:
- 自定義異常類,然后繼承自Exception 或者RunTimeException
- 實(shí)現(xiàn)一個(gè)帶有String類型參數(shù)的構(gòu)造方法,參數(shù)含義:出現(xiàn)異常的原因
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
此時(shí)我們的 login 代碼可以改成文章來源:http://www.zghlxwxcb.cn/news/detail-631310.html
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
public class LogIn {
private String userName = "admin";
private String password = "123456";
public static void loginInfo(String userName, String password)
throws UserNameException,PasswordException{
if (!userName.equals(userName)) {
throw new UserNameException("用戶名錯(cuò)誤!");
}
if (!password.equals(password)) {
throw new PasswordException("密碼錯(cuò)誤!");
}
System.out.println("登陸成功");
}
public static void main(String[] args) {
try {
loginInfo("admin", "123456");
} catch (UserNameException e) {
e.printStackTrace();
} catch (PasswordException e) {
e.printStackTrace();
}
}
}
注意事項(xiàng)
- 自定義異常通常會(huì)繼承自 Exception 或者 RuntimeException
- 繼承自 Exception 的異常默認(rèn)是受查異常
- 繼承自 RuntimeException 的異常默認(rèn)是非受查異常
總結(jié)
關(guān)于《 萬字帶你認(rèn)識(shí)異?!肪椭v解到這兒,javaSE系列的內(nèi)容也就到這兒了,感謝大家的支持,歡迎各位留言交流以及批評(píng)指正,如果文章對(duì)您有幫助或者覺得作者寫的還不錯(cuò)可以點(diǎn)一下關(guān)注,點(diǎn)贊,收藏支持一下。文章來源地址http://www.zghlxwxcb.cn/news/detail-631310.html
到了這里,關(guān)于【javaSE】 萬字帶你認(rèn)識(shí)異常的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!