Java基礎——異常(Exception)

【後續內容:異常、集合、常用類,其實都是在面向對象的基礎上,(即:我們需要解決一個實際的問題,Java爲我們提供了一套API,API中可能有很多類、接口,它們之間有複雜的繼承、實現關係),我們要做的就是學習這些類、接口,他們之間的關係,以及他們當中的一些方法】


一、異常概述:

  • 異常:程序執行過程中的"不正常"情況,(開發過程中的語法錯誤、邏輯錯誤,不算異常)
    【編寫代碼的過程中,不可能一點bug不出,很多問題並不是靠編碼就能解決的,比如:用戶輸入的格式不對(整型故意或無意輸錯成String型),讀取的文件不存在(誤刪了),網絡是否保持暢通(斷網了)】

  • 錯誤分兩類:

    1. Error:Java虛擬機也無法解決的嚴重問題,如:JVM系統內部錯誤(虛擬機自己都崩了)、資源耗盡等嚴重情況,這種問題一般不專門編寫新代碼來處理;
      【解決不了,必須回頭重寫對應部分的源代碼】
      【例如:棧溢出 StackOverflowError,堆溢出 OutOfMemoryError(OOM)】
    2. Exception:其他因編程錯誤或偶然的外因造成的一般性問題,這種問題可以通過編寫針對性的代碼來處理;
      【例如:空指針異常、試圖讀取的文件不存在、網絡連接中斷、數組下標越界】

【平時所說的異常都是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));
}
  • 兩種解決方案:
    1. 遇到錯誤,直接終止程序(即:啥也不做);
    2. 提前考慮到錯誤的檢測、錯誤消息的提示,以及解決方案

【就像:出去旅遊,途中生病了,①直接躺了,啥也幹不了了;②或者提前考慮到可能感冒、肚子疼,帶好了藥,如果途中沒生病,更好,如果生病了,該吃藥吃藥】

  • 異常(Exception)的分類:
    1. 編譯時異常:
    2. 運行時異常:

【處理錯誤的最佳時期是編譯時,但有些錯誤只有在運行時纔會發生(比如:除數爲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】

用法:

  1. finally是可選的
  2. 使用try將可能出現的異常代碼包裝起來,在執行過程中,一旦出現異常,就會生成一個對應異常類的對象,根據此對象的類型,去catch中依次匹配
  3. 一旦try中的異常對象匹配到某一個catch時,就進入catch中進行異常的處理(進入一個catch後,不會再進入其他catch);一旦處理完成,就跳出當前try-catch結構(沒有finally的情況下),繼續執行try-catch之後的代碼
  4. catch中的異常類型之間,若無子父類關係,則誰先誰後無所謂;若存在子父類關係,子類必須在前面,否則會報錯(跟if-else稍有區別,不僅是執行不到,乾脆編譯就不通過)
  5. catch中常用的異常對象的處理方法:① String getMessage();② void printStackTrace()
  6. 在try結構中聲明的變量,作用域就在try內,外部不可使用(想使用,就try外聲明,try內賦值)
  7. try-catch-finally結構可以嵌套使用(比如案例代碼)

注意:

  • 體會1:使用try-catch-finally處理編譯時異常,使得程序在編譯時不再報錯,但運行時仍可能出現異常
    【相當於使用try-catch將編譯時異常,延遲到了運行時出現】
  • 體會2:實際開發中,通常也不會去處理運行時異常
    【因爲運行時異常比較常見,編譯時也不會報錯、很難發現,而且加不加try-catch,其實最後都是拋出一堆紅字。所以通常不處理,出現異常了就回來改代碼得了】
    (當然,編譯時異常必須處理,不然編譯都過不了,程序跑都跑不起來)

finally的作用(finally中的代碼是一定會被執行的)

  • 【這跟不在finally中寫,而直接寫在try-catch後面有什麼區別?】
    finally中的代碼是一定會被執行的,即使catch中又出現了異常,或者try中&catch中有return語句
    1. catch中的代碼可能還有異常(出現異常後,沒有處理,方法直接結束了,try-catch後的代碼也不會執行,但是finally中的一定會執行)
    2. 方法有返回值的話,try中、catch中有return語句,在return之前,finally中的代碼也一定要執行
    3. 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 + 異常類型

用法:

  1. "throws + 異常類型"寫在方法聲明處。指明該方法執行時,可能會拋出的異常類型;
    【一旦方法執行時出現了異常,仍會在異常代碼處生成一個異常類的對象,此對象若滿足throws的異常類型,就會被拋出】
  2. 誰調用該方法,就拋給誰
    【對於當前方法而言,該異常就相當於處理結束,(遇到異常,後續代碼不會執行)】
  3. 調用該方法的方法,可以繼續往上拋,直到main()方法;
    【雖然main方法也可以繼續拋,但那就是拋給JVM了,相當於完全沒處理,所以一般情況下至少最終要在main方法中處理)】
  4. 調用的方法也可以自行 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):
    1. 繼承於現有的異常結構:RuntimeException、Exception(前者是運行時異常,編譯時不必處理;後者則必須顯式處理,否則報錯)
    2. 提供一個全局常量:serialVersionUID(序列號,可以理解爲該類的一個"唯一標識",後期講到IO流中的對象流會說)
    3. 提供幾個重載的構造器

代碼實例:

//見名知意
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()】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章