文章目錄
【後續內容:異常、集合、常用類,其實都是在面向對象的基礎上,(即:我們需要解決一個實際的問題,Java爲我們提供了一套API,API中可能有很多類、接口,它們之間有複雜的繼承、實現關係),我們要做的就是學習這些類、接口,他們之間的關係,以及他們當中的一些方法】
一、異常概述:
-
異常:程序執行過程中的"不正常"情況,(開發過程中的語法錯誤、邏輯錯誤,不算異常)
【編寫代碼的過程中,不可能一點bug不出,很多問題並不是靠編碼就能解決的,比如:用戶輸入的格式不對(整型故意或無意輸錯成String型),讀取的文件不存在(誤刪了),網絡是否保持暢通(斷網了)】 -
錯誤分兩類:
- Error:Java虛擬機也無法解決的嚴重問題,如:JVM系統內部錯誤(虛擬機自己都崩了)、資源耗盡等嚴重情況,這種問題一般不專門編寫新代碼來處理;
【解決不了,必須回頭重寫對應部分的源代碼】
【例如:棧溢出 StackOverflowError,堆溢出 OutOfMemoryError(OOM)】 - Exception:其他因編程錯誤或偶然的外因造成的一般性問題,這種問題可以通過編寫針對性的代碼來處理;
【例如:空指針異常、試圖讀取的文件不存在、網絡連接中斷、數組下標越界】
- Error:Java虛擬機也無法解決的嚴重問題,如:JVM系統內部錯誤(虛擬機自己都崩了)、資源耗盡等嚴重情況,這種問題一般不專門編寫新代碼來處理;
【平時所說的異常都是Exception,因爲Error是無法處理的嚴重錯誤,不考慮去專門處理,也處理不了,只能去重寫錯誤源碼】
public static void main(String[] args) {
//棧溢出:java.lang.StackOverflowError
main(args); //遞歸調用,沒有出口
//堆溢出:java.lang.OutOfMemoryError
int[] arr = new int[1024 * 1024 * 1024];
//空指針異常:java.lang.NullPointerException
String str = null;
System.out.println(str.charAt(0));
}
- 兩種解決方案:
- 遇到錯誤,直接終止程序(即:啥也不做);
- 提前考慮到錯誤的檢測、錯誤消息的提示,以及解決方案
【就像:出去旅遊,途中生病了,①直接躺了,啥也幹不了了;②或者提前考慮到可能感冒、肚子疼,帶好了藥,如果途中沒生病,更好,如果生病了,該吃藥吃藥】
- 異常(Exception)的分類:
- 編譯時異常:
- 運行時異常:
【處理錯誤的最佳時期是編譯時,但有些錯誤只有在運行時纔會發生(比如:除數爲0,數組下標越界)】
二、異常體系結構
【圖中Exception的子類中:紅色爲編譯時異常,藍色爲運行時異常】
補充:面試題:常見異常都有哪些?舉例說明
* java.lang.Throwable
* |-----java.lang.Error:(一般不處理)
* |-----java.lang.Exception:(需要處理)
* |-----編譯時異常(checked)【編譯就不通過,提示"未處理xxx異常"(比如使用FileInputStream時,不try-catch,就會報錯,提示沒處理FileNotFoundException),單純的語法寫錯了不叫編譯時異常】
* |-----IOException
* |-----FileNotFoundException
* |-----ClassNotFoundException
* |-----運行時異常(unchecked,RuntimeException)
* |-----NullPointerException
* |-----ArrayIndexOutOfBoundsException
* |-----ClassCastException
* |-----NumberFormatException
* |-----InputMismathException
* |-----ArithmeticException
三、異常的處理模型:抓拋模型
3.1 過程一:“拋”
- "拋":程序在正常執行過程中,一旦出現異常,就會在異常代碼處生成一個對應異常類的對象,並將此對象拋出【拋給程序調用者】
(一旦拋出異常對象後,其後的代碼就不再執行了)
3.2 過程二:“抓”
- "抓":即抓住這個被拋出的異常,可以理解爲異常的處理方式(抓住之後怎麼做):① try-catch-finally;② throws
四、異常的處理方式
4.1 爲什麼要有專門的異常處理?
程序運行過程中,可能出現很多種異常(除數爲0、數據爲空…),如果每個地方都添加if-else來檢測處理,會導致代碼臃腫、可讀性很差,因此使用try-catch的方式,將可能有異常的進行集中處理,與正常代碼區分開,使代碼簡潔、易於維護】
- 方式一:try-catch-finally
- 方式二:throws + 異常類型
4.2 方式一:try-catch-finally
【整體上類似if-else或switch-case】
用法:
- finally是可選的
- 使用try將可能出現的異常代碼包裝起來,在執行過程中,一旦出現異常,就會生成一個對應異常類的對象,根據此對象的類型,去catch中依次匹配
- 一旦try中的異常對象匹配到某一個catch時,就進入catch中進行異常的處理(進入一個catch後,不會再進入其他catch);一旦處理完成,就跳出當前try-catch結構(沒有finally的情況下),繼續執行try-catch之後的代碼
- catch中的異常類型之間,若無子父類關係,則誰先誰後無所謂;若存在子父類關係,子類必須在前面,否則會報錯(跟if-else稍有區別,不僅是執行不到,乾脆編譯就不通過)
- catch中常用的異常對象的處理方法:① String getMessage();② void printStackTrace()
- 在try結構中聲明的變量,作用域就在try內,外部不可使用(想使用,就try外聲明,try內賦值)
- try-catch-finally結構可以嵌套使用(比如案例代碼)
注意:
- 體會1:使用try-catch-finally處理編譯時異常,使得程序在編譯時不再報錯,但運行時仍可能出現異常
【相當於使用try-catch將編譯時異常,延遲到了運行時出現】 - 體會2:實際開發中,通常也不會去處理運行時異常
【因爲運行時異常比較常見,編譯時也不會報錯、很難發現,而且加不加try-catch,其實最後都是拋出一堆紅字。所以通常不處理,出現異常了就回來改代碼得了】
(當然,編譯時異常必須處理,不然編譯都過不了,程序跑都跑不起來)
finally的作用(finally中的代碼是一定會被執行的)
- 【這跟不在finally中寫,而直接寫在try-catch後面有什麼區別?】
finally中的代碼是一定會被執行的,即使catch中又出現了異常,或者try中&catch中有return語句- catch中的代碼可能還有異常(出現異常後,沒有處理,方法直接結束了,try-catch後的代碼也不會執行,但是finally中的一定會執行)
- 方法有返回值的話,try中、catch中有return語句,在return之前,finally中的代碼也一定要執行
- GC機制只能回收JVM堆內存中的對象空間,對於其他的物理連接(比如:數據庫連接、IO流、Socket連接等)無能爲力,必須我們手動釋放這些連接資源
【即使出現異常,也必須要關掉這些連接,否則會造成"內存泄露" ----> 放在finally中】
【快捷操作:選中異常代碼部分,右鍵—>Surround With —> Try/catch Block】
代碼實例:
//try-catch方式處理異常
@Test
public void test2(){
FileInputStream fis = null;
try{
File file = new File("hello.txt");
fis = new FileInputStream(file);
int data = fis.read();
while(data != -1){
System.out.println(data);
data = fis.read();
}
}catch(FileNotFoundException e){
System.out.println("文件未找到異常!");
//catch中常用的兩個方法:
System.out.println(e.getMessage());
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
try {
if(fis != null) //爲了避免空指針異常(fis本身也可能出異常,即:根本沒創建成功)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.3 方式二:throws + 異常類型
用法:
- "throws + 異常類型"寫在方法聲明處。指明該方法執行時,可能會拋出的異常類型;
【一旦方法執行時出現了異常,仍會在異常代碼處生成一個異常類的對象,此對象若滿足throws的異常類型,就會被拋出】 - 誰調用該方法,就拋給誰;
【對於當前方法而言,該異常就相當於處理結束,(遇到異常,後續代碼不會執行)】 - 調用該方法的方法,可以繼續往上拋,直到main()方法;
【雖然main方法也可以繼續拋,但那就是拋給JVM了,相當於完全沒處理,所以一般情況下至少最終要在main方法中處理)】 - 調用的方法也可以自行 try-catch 處理,這樣該異常就在這一層被解決了,再往上層的調用就不會有異常了
注意:
- 體會:throws本質上相當於沒有處理掉異常,只是拋給調用者來處理,治標不治本;而try-catch-finally纔是真正地將異常處理掉了
代碼實例:
//throws測試
public static void main(String[] args) {
//最終要在main()方法中解決,(不要再繼續拋了,再拋就又拋給JVM了)
try {
method4();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void method4() throws IOException{
method3();
}
public static void method3() throws FileNotFoundException, IOException{
File file = new File("hello.txt");
FileInputStream fis = new FileInputStream(file);
int data = fis.read();
while(data != -1){
System.out.println(data);
data = fis.read();
}
fis.close();
}
補充:爲什麼子類中重寫的方法,拋出的異常必須比父類拋出的異常小?
【因爲體現多態性的時候,方法調用,形參是父類對象,方法中就要對父類拋出的異常進行處理(例如:IOException);如果子類中拋出的異常比父類小(例如:FileNotFoundException),那麼到這裏,catch依然能罩得住;但如果比父類異常大(例如:Exception),那麼在該方法內,catch(IOException e)就罩不住了】
【相當於語言設計本身有問題了:我明明都把異常處理了,你還報異常】
4.4 開發中如何選擇使用try-catch-finally還是throws
- 如果父類中被重寫的方法沒有throws拋出異常,則子類中重寫的方法也不能throws(因爲要求必須比父類異常要"小"),這就意味着如果子類的重寫方法中出現了異常,只能使用try-catch自行處理,不能向上throws
- 執行的方法func()中,先後調用了另外的幾個方法(例如:A()、B()、C()),而這幾個方法之間是遞進關係的(例如:A的運行結果,作爲B的參數,B的結果,又作爲C的參數),
【此時我們建議:若A、B、C方法中有異常,不要自行進行try-catch,而是throws,由調用他們的方法func()來try-catch處理】
【因爲:①各自處理,代碼繁瑣,不如統一處理了;②A、B、C是遞進關係,A出現了異常的話,通常得出的結果對B來說也用不了,如果A中處理了異常,那麼程序就能正常往下執行,結果就會傳給B,然而結果已經錯誤了,傳給B也沒用,這樣邏輯上也不合適】
注意
try-catch異常處理只是爲了處理編譯時異常,真要在運行時出現了異常,給個友好提示,最終還是要找到錯誤原因,回去改代碼
【即:一旦出現問題,給用戶一個友好的展示,而不是一堆亂碼;實際上還是要發到後臺,進行記錄,然後由我們去修改源碼,讓其不再有異常出現】
【即:目的仍然是,例如:當用戶點了一個按鈕以後,出現的是該出現的界面,而不是一個友好的提示"出現問題了"】
五、手動拋出異常
關於異常對象的產生:
- 系統自動生成的異常對象(上述所有異常都是如此)
- 手動生成一個異常對象,並拋出(throw)
【一般throw的都是Exception或者RuntimeException】
注意:區分 throw 和 throws
- throw是在"拋"的過程中,產生異常對象的一種方式;
- throws是在"抓"的過程中,處理異常的一種方式;
public class ThrowTest {
public static void main(String[] args) {
try {
Student stu = new Student();
stu.register(-1001); //輸入數據非法的時候,後續代碼不應該執行了,(否則容易誤認爲是 "id=0")
System.out.println(stu);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
class Student{
private int id;
public void register(int id) throws Exception{
if(id > 0){
this.id = id;
}else{
// System.out.println("輸入數據非法!");
// throw new RuntimeException("輸入數據非法!");
throw new Exception("輸入數據非法!");
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
六、自定義異常
- 如何自定義異常類(仿照已有的異常類,例如:Exception):
- 繼承於現有的異常結構:RuntimeException、Exception(前者是運行時異常,編譯時不必處理;後者則必須顯式處理,否則報錯)
- 提供一個全局常量:serialVersionUID(序列號,可以理解爲該類的一個"唯一標識",後期講到IO流中的對象流會說)
- 提供幾個重載的構造器
代碼實例:
//見名知意
class NegativeNumberException extends Exception{
static final long serialVersionUID = -7034897196220766939L; //序列號
public NegativeNumberException() {
super();
}
public NegativeNumberException(String message) {
super(message);
}
}
class Student{
private int id;
public void register(int id) throws Exception{
if(id > 0){
this.id = id;
}else{
//不能輸入負數
throw new NegativeNumberException("不能輸入負數!");
}
}
}
七、總結
用如上5個關鍵字,就能總結異常部分的總體內容
注意:throw 和 throws 的區別
- throw是"拋"異常的過程中,生成異常對象的一種方式;throws則是"抓"異常的過程中,處理異常的一種方式
【throw:我還沒異常對象呢,怎麼生成一個對象;throws:已經出現異常對象了,我怎麼處理】
【二者的關係:類似於"上游排污,下游治污"】
面試題:final、finally、finallize()三者的區別
- 三者沒啥關係,分開說清楚就可以了
【類似的一系列面試題(結構很相似的,可能真就沒啥關係):throw和throws;Collection和Collections;String、StringBuffer和StringBuilder;ArrayList和LinkedList;HashMap和LinkedHashMap;重寫、重載】
【還有另外一系列面試題(結構不相似的,反而可能有相似之處):抽象類、接口;== 和 equals();sleep()和wait()】