目錄
沒有花裡胡哨,純乾貨,小白也能看懂的異常最詳細總結!!
java例外處理的五個關鍵字:try,catch,finally,throw,throws
在理想的狀態下,使用者輸入資料的格式永遠都是正確的,選擇開啟的檔案也一定存在,並且永遠不會出現bug。然而,在現實世界中卻充滿了不良的資料和帶有問題的程式碼。
如果一個使用者在執行程式期間,由於程式的錯誤或一些外部環境的影響造成使用者資料的丟失,使用者就有可能不再使用這個程式了。為了避免這類事情的發生,至少應該做到以下幾點:
向使用者通告錯誤;
儲存所有工作結果;
允許使用者以妥善的形式退出程式。
Java使用一種稱為例外處理(exception handing)的錯誤捕獲機制處理。
異常指的是什麼?
異常字面上就是不正常的意思。
在程式中的意思就是
異常:即指在程式執行的過程中,出現非正常情況,最終導致JVM的非正常停止。
在Java等物件導向的程式語言中,異常本身是一個類,產生異常就是建立一個異常物件並丟擲一個異常物件。Java虛擬機器器處理異常的方式就是中斷處理。
異常指的不是語法錯誤,語法錯誤時,編譯不通過,不會產生位元組碼檔案,根本不能執行。
異常機制是在幫我們找到程式中的問題
異常的根類是java.lang.Throwable
這個根類下有兩個子類,分別是java.lang.Error java.lang.Exception,平常我們所說的異常即java.lang.Exception。
Throwable體系
1,Error:嚴重錯誤Error,無法處理,只能事先避免,相當於絕症這種無法治癒的問題。必須修改原始碼,程式才能繼續執行。
2,Exception:表示異常,異常產生後,程式設計師可以通過程式碼去糾正,使得程式繼續去執行,相當於感冒發燒這種小毛病,進行處理後可以恢復。
java.lang.Throwable類是Java中所有異常或者錯誤的超類。
一般Exception指的是編譯期異常,進行編譯(寫程式碼時)java程式出現的問題。
其中Exception下有一個特殊的子類:RuntimeException指的是執行期異常。即程式執行的時候丟擲的異常。
Demo1
產生了編譯期異常:
throws關鍵字
通過throws關鍵字宣告丟擲這個異常,交給方法的呼叫者去處理,在這裡main方法的呼叫者是JVM,即交給JVM去處理。
新增了 throws ParseException後,此時發現紅線沒有了,程式可以正常執行了。
注意:此時我們的"2022-01-01"與它的"yyyy-MM-dd"格式是一致的,所以只要解決編譯時的異常就可以正常執行程式。
那麼當我們把格式改成不一致的時候,即格式不匹配,比如給它一個"2022-0101",它還會丟擲異常。
因為此時我們使用的是第一種處理異常的方式,即交給JVM虛擬機器器去處理,而虛擬機器器處理的方式就是中斷程式,並把異常列印出來,所以出現異常的語句後面的語句就無法執行了,若我們想讓出現異常的語句後的語句依然繼續執行,我們需要來了解第二種例外處理方式。
try{可能會出現異常的程式碼
} catch(Exception e){異常的處理邏輯}
此時可以看到,除了列印了異常的資訊也執行了後續的程式碼
再來看一下執行期異常:
此程式碼編譯時並不會有錯誤提醒,但是在執行中,很明顯會產生索引越界異常 ,這就是執行期異常。我們依然可以使用try,catch來處理這個異常。處理之後,依然可以執行後續程式碼。
當空間數為1024時,此時是沒有問題的,也可以執行到後續程式碼。
但是當我們把空間數增加為1024*1024*1024時
此時出現了一個以Error結尾的OutOfMemoryError,這就是一個錯誤,名稱為記憶體溢位錯誤,即建立的陣列太大,超出了給JVM分配的記憶體。產生錯誤必須修改原始碼,否則是不會繼續執行下去的,在這裡即把陣列修改的小一點就可以了。
再來看一個例子:
因為我定義的陣列下標最大為2,很明顯,此時會產生異常
程式執行的結果:
仔細觀察,我們可以發現,異常是在 int ele = arr[index]; 這一行程式碼產生的
這時存取了陣列中的3索引(下標),但是陣列中並沒有3索引,這時JVM就會檢測出程式出現了異常。
1,
JVM會做兩件事:
1)JVM會根據異常產生的原因建立一個異常物件,這個異常物件包含了異常產生的(內容,原因,位置) new ArrayIndexOutOfBoundsException("3");
2)在getElement方法中,沒有異常的處理邏輯(try,catch),那麼JVM就會把異常物件丟擲給方法的呼叫者,也就是讓main方法來處理異常
getElement方法把異常物件丟擲給main方法
2,
回到main方法中的這行語句,int e =getElement(arr,3);
main方法接收到了這個異常物件(new ArrayIndexOutOfBoundsException("3")),但是main方法也沒有異常的處理邏輯,繼續把物件丟擲給main方法的呼叫者,即JVM處理
main方法把異常物件丟擲給 JVM
3,
JVM接收到了這個異常物件(new ArrayIndexOutOfBoundsException("3")),做了兩件事情:
1,把異常物件(內容,原因,位置)以紅色的字型列印在控制檯
2,JVM會終止當前正在執行的java程式 ——>中斷處理
接下來我們挨個來介紹:
關於throw關鍵字的介紹
作用:使用throw關鍵字可以在指定方法中丟擲指定的異常 使用格式: throw new xxxException("異常產生的原因"); 注意事項: 1,throw關鍵字必須寫在方法的內部 2,throw關鍵字後邊new的物件必須是Exception或者Exception的子類物件 3,throw關鍵字丟擲指定的異常物件,我們就必須處理這個異常物件 throw關鍵字後邊建立的是RuntimeException或者是RuntimeException的子類物件我們可以不處理,預設交給JVM去處理 throw關鍵字後邊建立的是編譯器異常,我們就必須處理這個異常,要麼throws,要麼try...catch
我們依然用一個例子來解釋它:
執行結果:
這是我並沒有處理這個異常,那它是誰處理的呢?
上邊我們提到了
throw關鍵字後邊建立的是RuntimeException或者是RuntimeException的子類物件我們可以不處理,預設交給JVM去處理
此時的NullPointerException就是一個執行期異常,即RuntimeException的子類,我們不用處理,預設交給JVM去處理
小tips: 在工作中,我們首先必須對方法傳遞過來的引數做合法性校驗 如果引數不合法,那麼我們就必須要使用丟擲異常的方式,告訴方法的呼叫者,傳遞的引數有問題
在上邊的例子中我們判斷了陣列arr的值是否為空,我們還有另外一個引數,即index,我們接著再來對index進行合法性校驗。
把上邊的空陣列改為
此時若傳遞引數為(arr,3)
會丟擲ArrayIndexOutOfBoundsException,即陣列索引越界異常
ArrayIndexOutOfBoundsException也是一個執行時異常,預設交給JVM去處理。
此種方法即宣告異常
throws關鍵字:是例外處理的第一種方式,即交給別人去處理
作用:
當方法內部丟擲異常物件時,我們就必須處理這個異常物件
可以使用throws關鍵字進行例外處理,會把異常物件丟擲給方法的呼叫者處理(自己不處理,交給別人處理),若沒人處理,最終交給JVM處理——>中斷處理
使用格式:在方法宣告時使用
修飾符 返回值型別 方法名(參數列)throws AAAException,BBBException...{
throw new AAAException("產生異常的原因");
throw new BBBException("產生異常的原因");
....
}
注意事項:
1,throws關鍵字必須寫在方法宣告處
2,throws關鍵字後邊的異常必須是Exception或者Exception的子類
3,方法內部如果丟擲了多個異常物件,throws後面也必須宣告多個異常
如果丟擲的異常有子父類別關係,只需宣告父類別異常即可
4,呼叫一個宣告異常的方法,就必須處理宣告的異常
如何處理:1)繼續使用throws關鍵字進行宣告丟擲,交給方法的呼叫者處理,最終交給JVM處理
2)要麼try...catch自己處理異常
舉個栗子
定義一個方法對傳遞的檔案路徑進行一個合法性判斷
如果路徑不是"c:\\.java.txt"我們就丟擲檔案找不到這個異常( ),告訴方法的呼叫者
此時發現程式已經標了紅線,原因是FileNotFoundException是編譯器異常,上面我們說過,只要出現編譯期異常,我們就必須進行處理
此時就可以使用throws關鍵字繼續宣告丟擲FileNotFoundException這個異常物件,讓方法的呼叫者來處理。
接著我們來補全main方法來呼叫readFile方法
此時我傳給readFile的是正確的路徑,但是發現readFile仍然下邊依然有紅線 ,這是因為我們剛才介紹的注意事項的第四點
那我們就得在main來處理這個異常 ,我們依舊使用第一種方法,即繼續使用throws關鍵字進行宣告丟擲,此時main方法把異常物件交給它的呼叫者處理,即讓JVM去處理。
public class Demo5 {
public static void main(String[] args) throws FileNotFoundException {
readFile("c:\\.java.txt");
}
public static void readFile (String fileName)throws FileNotFoundException{
if (!fileName.equals("c:\\.java.txt")){
throw new FileNotFoundException("傳遞的檔案路徑不是c:\\.java.txt");
}
System.out.println("路徑沒有問題,讀取檔案");
}
}
此時程式碼就沒有問題了
我們再來加一個if語句,如果傳遞的路徑不是.txt結尾
我們丟擲IO異常物件,告訴方法的呼叫者,檔案的字尾名不對
此時我們把傳遞的檔案路徑字尾名改為.tx,它就會報IO異常,我們要像上邊宣告丟擲FileNotFoundException異常物件一樣,宣告丟擲IOException異常物件
注意:由於FileNotFoundException是IOException的子類,所以只需宣告丟擲IOException,即父類別異常即可!!
此種方法即捕獲異常
上邊我們介紹過的第一種例外處理方式-宣告異常,不難發現,它是有一定缺陷的。
如果我們在上面Demo5的例子中,給main方法中的readFile("c:\\.java.tx");
這條語句後邊加一個
System.out.println("後續程式碼");
即讓程式執行後續程式碼,發現後續程式碼是不能執行的。原因也很簡單,就是我們上邊講過的,若沒人去處理這個異常,最後會交給JVM去處理,而JVM處理的方式是中斷程式,所以後續程式碼自然就不能執行了。
而try...catch是自己去處理異常,後續程式碼也可以繼續執行。
try...catch,例外處理的第二種方式,自己處理異常
格式:(一個try中可以對應多個catch)
try{可能產生異常的程式碼
} catch(定義一個異常的變數,用來接收try中丟擲的異常物件){
異常的處理邏輯,產生異常之後,怎麼處理異常物件
一般在工作中,會把異常資訊記錄在紀錄檔中
}
...
catch(異常類名 變數名){ }
注意事項:
1,try中可能會出現多個異常物件,可以使用多個catch來處理這些異常物件
2,如果try中產生了異常,那麼就會執行catch中的例外處理邏輯,執行完catch中的例外處理邏輯,繼續執行try...catch後的程式碼
如果try中沒有產生異常,那麼不執行catch的例外處理邏輯,即執行完try中的語句,繼續處理try...catch後的程式碼
舉個栗子
依然是上邊的檔案路徑的例子,只是 此時我們的main方法在收到readFile傳遞的異常物件之後,不再宣告丟擲給JVM來處理,而是使用try...catch自己進行處理。
當傳遞的引數為正確的檔案路徑時,此時,程式沒有異常產生,不執行catch中的例外處理邏輯,程式正常執行。
此時列印:
當傳的引數為錯誤的檔案路徑時,此時,程式有異常產生,catch捕捉到try中產生的異常,並執行了例外處理邏輯,執行完catch後,程式依然繼續執行後續程式碼(不同於throws的地方)。
此時列印:
finally:有一些特定的程式碼無論異常是否發生,都需要執行。另外,因為異常會導致程式跳轉,導致有些語句執行不到。finally就是用來解決這個問題的,放在finally中的語句塊一定會被執行到。
我們寫在try中的程式碼如果出現了異常,就會直接把異常拋給catch來處理,那麼在try中出現異常的位置之後的程式碼就是執行不到的。
如圖:
此時我想列印這個"釋放空間",是執行不到的,因為產生了異常,直接跳到了catch語句中 。
如果我想把"釋放空間"列印出來,此時就可以使用finally語句。
finally程式碼塊:
格式:
try{可能產生異常的程式碼
} catch(定義一個異常的變數,用來接收try中丟擲的異常物件){
異常的處理邏輯,產生異常之後,怎麼處理異常物件
一般在工作中,會把異常資訊記錄在紀錄檔中
}
...
catch(異常類名 變數名){
}finally{
無論是否出現異常都會執行}
注意事項:
1,finally必須和try一起使用,不能單獨使用
2,finally一般用於資源釋放(資源回收),無論程式是否出現異常,最後都要資源釋放
多個異常如何捕獲與處理?
共有三種方法:
1,多個異常分別處理。
2,多個異常一次捕獲,多次處理。
3,多個異常一次捕獲,一次處理。
即有一個異常就要寫一個try...catch
即格式為
try{
}catch(){
}
try{
catch(){
}
System.out.println("後續程式碼")
此種方式有個優點:就是可以執行到後續程式碼
即一個try對應多個catch
即格式為
try{
}catch(){
......
} catch() {
}
此種方法使用時要注意:
catch裡邊定義的異常變數,如果有子父類別關係,那麼包含子類異常變數的catch語句必須寫在父類別的上邊,否則會報錯。
例如如圖的情況,就報錯了。
原因是:
例如:try中可能會產生以下兩個異常物件:
new ArrayIndexOutOfBoundsException("3");
new IndexOutOfBoundsException("3");
try中如果出現了異常物件,會把異常物件丟擲給catch處理
丟擲的異常物件,會從上到下賦值給catch中定義的異常變數。
如果父類別異常變數的catch語句在子類的上邊,此時無論是產生子類異常還是產生父類別異常,都會賦給父類別catch語句中的異常變數(多型的體現),而下邊子類catch語句中的異常變數就沒有被使用所以會報錯,這並不是我們想要的結果。
所以,在catch裡邊定義的異常變數,如果有子父類別關係,那麼包含子類異常變數的catch語句必須寫在父類別的上邊。
即只有一個try和一個catch
即格式為
try{
}catch(此時這裡的異常變亮一般為父類別異常即可以處理多個異常物件或者直接寫Exception){
}
特殊的:執行時異常(RuntimeException)可以不處理也不宣告丟擲
預設交給虛擬機器器去處理,終止程式,什麼時候不丟擲執行時異常了,再執行程式。
如果finally中有return語句,永遠返回finally中的結果,我們應該避免該種情況。
public class Demo6 {
public static void main(String[] args) {
int a =getA();
System.out.println(a);
}
public static int getA(){
int a = 10;
try{return a;
}catch(Exception e){
System.out.println(e);
}finally {
a = 100;
return a;
}
}
}
此時列印結果為100,我們應該去避免這種情況的發生,即不在finally裡寫return語句。
關於子父類別的異常問題
(此部分程式碼較簡單,不進行演示,只要記住下邊兩條,自然就會使用了)
1)如果父類別丟擲了多個異常,子類重寫父類別方法時,丟擲和父類別相同的異常,或者是父類別異常的子類或者是不丟擲異常。
2)父類別方法沒有丟擲異常,子類重寫父類別該方法時也不可丟擲異常,此時子類產生該異常,只能捕獲處理,而不能宣告丟擲。
Java中的不同的異常類,分別表示著某一種具體的異常情況。但是在具體的開發過程中,我們總會用到一些Java中沒有的異常類,比如我們要考慮考試成績是負數的問題。這時就需要我們自己去定義一個異常類。
在開發中自己業務的異常情況來定義異常類。
自定義一個業務邏輯異常:RegisterException,即一個註冊異常類。
1,自定義一個編譯期異常:自定義類並繼承於java.lang.Exception。
2,自定義一個執行時期的異常類:自定義類並繼承於java.lang.RuntimeException
格式:
public class Exception extends Exception/RuntimeExcetion{
新增一個空引數的構造方法
新增一個帶異常資訊的構造方法
}
注意:
1,自定義異常類一般都是以Exception結尾,說明該類是一個異常類
2,自定義異常類,必須得繼承自Exception或者RuntimeException
繼承自Exception:那麼定義的異常類就是一個編譯期異常,如果方法內部丟擲了編譯期異常,就必須處理這個異常,要麼throws ,要麼try ...catch
繼承自RuntimeException:那麼定義的異常就是一個執行期異常,無需處理,交給虛擬機器器處理(中斷處理)
下面我們來自定義一個異常類
public class RegisterException extends Exception {
// 新增一個空引數的構造方法
// public RegisterException(){}
public RegisterException(){super();}
// 實際上我們此時預設呼叫的是空參的父類別的構造方法,以上兩條語句等價
/*新增一個帶異常資訊的構造方法,這個怎麼新增呢
我們可以參照一下jdk中的NullpointerException原始碼中的構造方法
檢視NullpointerException的原始碼後發現,所有異常類都會有一個帶異常資訊的構造方法
在方法內部會呼叫父類別帶異常資訊的構造方法,讓父類別來處理這個異常資訊*/
public RegisterException (String message){
super(message);
}
}
我們使用上面我們定義好的異常類RegisterException 進行練習
import java.util.Scanner;
/*要求:模擬註冊操作,如果使用者名稱已存在,丟擲異常並提示,該使用者名稱已被註冊。
分析:
1,使用陣列儲存註冊過的使用者名稱
2,使用Scanner獲取使用者輸入的註冊的使用者名稱
3,定義一個方法,對使用者輸入的註冊的使用者名稱進行判斷
遍歷儲存已經註冊過使用者名稱的陣列,獲取每一個使用者名稱
使用獲取到的使用者名稱和使用者輸入的使用者名稱比較
true:
使用者名稱已經存在,丟擲RegisterException,告知使用者該使用者名稱已經註冊
false:
繼續遍歷比較
如果迴圈結束,還沒找到重複的,提示使用者,註冊成功!
*/
public class RegisterException2 {
static String[] usernames = {"張三","李四","王五"};
public static void main(String[] args) throws RegisterException {
Scanner sc = new Scanner(System.in);
System.out.println("請輸入你要註冊的使用者名稱");
String username = sc.next();
checkUsername(username);
}
public static void checkUsername(String username) throws RegisterException {
for (String name:usernames) {
if (name.equals(username)){
throw new RegisterException("該使用者名稱已經註冊");
}
}
System.out.println("註冊成功!");
}
}
}
此時,我們輸入不存在的使用者名稱,執行結果如下
輸入已經存在的使用者名稱,執行結果如下
達到了我們想要的結果。
上邊使用的是throws一直宣告丟擲,最終交給了JVM去處理。
我們也可以使用try... catch來處理
public class RegisterException2 {
static String[] usernames = {"張三","李四","王五"};
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("請輸入你要註冊的使用者名稱");
String username = sc.next();
checkUsername(username);
}
public static void checkUsername(String username) {
for (String name:usernames) {
if (name.equals(username)){
try {
throw new RegisterException("該使用者名稱已經註冊");
} catch (RegisterException e) {
e.printStackTrace();
}
}
}
System.out.println("註冊成功!");
}
}
執行結果如下
此時我們發現了一個問題,即當我輸入已存在的使用者名稱,程式丟擲異常後,依然會列印註冊成功,這顯然不符合預想。
如果丟擲了異常,我們應該讓方法停下來,不再繼續執行後面的語句
這裡只需要在catch裡新增一個return即可。
剛才我們繼承的是Exception,現在讓我們自定義的RegisterException再來繼承一下RuntimeException。
繼承之後,發現及時不加try..catch語句,也不宣告,程式也不會報錯,原因很簡單,就是我們在上邊一直在講的,丟擲RuntimeException(執行期異常)可以不處理,預設交給JVM來處理(中斷程式)。
執行結果
Throwable類中定義了三個處理異常的方法
分別是以下三個:
String | getMessage() 返回此 throwable 的簡短描述。 |
String | toString() 返回此 throwable 的詳細訊息字串 |
void |
JVM列印異常物件預設使用此方法,列印的異常 資訊是最全面的 |
舉個栗子
我們分別來列印它們進行觀察
依然使用上邊的程式碼Demo5例子,我們此時給它傳遞一個錯誤的檔案路徑(字尾名是錯誤的)。我們來看一下三種處理異常的方法的區別。
1)get Message方法
此時列印:
可以看到只有很簡短的描述
2)toString 方法
此時列印:
可以看到比上邊的getMessage方法詳細了一點
3)printStackTrace方法
此時列印:
可以看到此時列印了最詳細的異常資訊。
Objects類是一個由一些靜態的實用方法組成的類,這些方法是non-save(空指標安全的)或non-tolerant(容忍空指標的),那麼在它的原始碼中,對物件為null的值進行了拋異常操作。
Objects類中的靜態方法
public static <T> T requireNonNull(T obj):檢視指定參照物件不是null
原始碼:
public static <T> T requireNonNull(T obj){
if(obj == null){
throw new NullPointerException();
return obj;
}
舉個栗子:
上邊的程式碼可對傳過來引數進行合法性校驗,判斷其是否為空
當我們瞭解了 Objects類的requireNonNull方法後,可對程式碼進行一個簡化
即把註釋掉的這兩行if語句替換成了Objects.requireNonNull(obj);
以後如果我們在合法性判斷時,如果要判斷它是否為空,可以直接用Objects類裡的靜態方法即requireNonNull,可以簡化書寫程式碼。
到這裡,異常部分全部介紹完畢,本人才疏學淺,若各位發現錯誤,請盡情批評指正!!!
若對您有幫助,請點贊,收藏,加關注!!!我們一起努力!!謝謝!!!