# Java中的異常控制
## 一、前言概述
寫程序時發生錯誤在所難免,對錯誤的控制一直是編程人員要解決的一大問題,Java中的異常控制就為了幫助我們規避在語言層面可能發生的錯誤(業務邏輯層面的錯誤還是我們程序員自己的鍋)。 java中的異常控制是java語言的強大之處的體現之一。
在很多編程語言中經常將“異常控制”模塊內置到程序設計語言本身,有時甚至內建到操作系統內,但是有時并不會強制用戶(程序員)使用,即不會在一些可能發生異常的情況下強制我們處理它。但是java與大多數編程語言不同的是它有時候將**強制我們處理**它們(異常),處理的方式是`拋出`和`捕獲`。而且就算我們不處理發生的異常,最后JVM也會幫我們處理(即中斷程序的運行,并在控制臺上進行顯示)。
> 由于很難設計出一套完美的錯誤控制方案,許多語言干脆將問題簡單地忽略掉,將其轉嫁給庫設計人員。對大多數錯誤控制方案來說,最主要的一個問題是它們嚴重依賴程序員的警覺性,而不是依賴語言本身的強制標準。《java編程思想》
## 二、 Java的異常體系
Java中“一切皆對象”,同樣的java中的異常(Exception)體系也是由各種各樣的類構成的。Java中異常的根類(除Object外最上面一層)是`java.lang.Throwable`,在其下面有兩個直接的子類:java.lang.`Error`與java.lang.`Exception`,而我們平常所說的異常指的就是Exception及其子類。
至于發生Error錯誤一般是屬于系統內部出現的問題,例如堆棧溢出(StackOverFlowError)、內存溢出(OutMemoryError)等,這些一般和JVM有關,有時候可能需要我們調整一些參數解決,但是錯誤的發生仍可能存在。
最后要注意的是異常類并不是只在java.lang包中,還可能出現在你自定義的包中,引人的第三方包中。
Java異常體系如下:

### 2.1 異常分類
Java中一般會將異常分為`免檢異常`和`必檢異常`。
#### 免檢異常:RuntimeException
指的是在編程過程中允許我們不用手動處理,可以交給JVM處理,而JVM的處理方式就是中斷程序運行。這類異常一般都是**運行時期的異常**(RuntimeException),即在程序運行時才可能出現,而且一般這種情況是我們程序員自己造成的,比如說數組越界異常、空指針的引用異常等等。
#### 必檢異常:Exception
在編程過程中必須要手動處理的,處理的方式可以是“拋出”或者是“捕獲”。如果是做“拋出”處理的話,最后一層可以拋出給JVM去處理(仍然是中斷處理),“捕獲”的話就是我們自己來手動處理。這類異常也稱為**編譯異常**。即在編譯時必須處理,如果不處理,編譯不能通過,就跟我們的語法錯誤一樣,編譯器會給我們報錯。
對于需要處理的異常,在IDEA中會給我們兩種選擇處理的方式(對于這兩種方式更進一步的還會在下面講到):

由上圖的【Java異常體系】可知,異常體系的基類是Exception,而Exception又是繼承Throwable,其實查看各種異常類的源碼可以發現,各種異常類中基本就只有構造方法和一個序列號ID,而構造方法里面又只調用父類的構造方法,這樣沿著繼承鏈反向查看回去,會發現**最終調用的還是Throwable中的構造方法。**
例如:空指針異常類中的內容

里面的構造方法:(IDEA里面鼠標移上按住Crtl+單擊就能進去看了)
~~~
?//NullPointerException類
?public NullPointerException() {
? ? ?super();
?}
?public NullPointerException(String s) {
? ? ?super(s);
?}
?//最終的Throwable類的構造方法
?public Throwable() {
? ? ?fillInStackTrace(); //此方法記錄此Throwable對象信息,了解當前線程的堆棧幀的當前狀態。
?}
?public Throwable(String message) {
? ? ?fillInStackTrace();
? ? ?detailMessage = message;
?}
~~~
#### Throwable中的常用方法
* public void printStackTrace():打印異常的詳細信息,包括異常的發生路徑,異常的原因,及異常的種類;
* public String getMessage():獲取異常發生的原因;
* public String toString():獲取異常的類型和異常的描述信息,現在已經不用了。
### 2.2 異常產生過程舉例
#### 舉例
下面舉一個數組越界異常(ArrayIndexOfBoundsException)來看看異常產生的大概過程。
1. 寫一個方法來獲取數組中的元素,傳遞的參數為一個數組和要獲取元素的索引
~~~
?public static int getElement(int[] nums, int index) {
? ? ? ? ?return nums[index];
? ? }
~~~
2. 接著在測試中傳入數組和要獲取元素的索引
~~~
?public static void main(String[] args){
? ? ?int[] nums = {1, 2, 3, 4, 5};
? ? ?System.out.println(getElement(nums, 5));
?}
~~~
3. 因為索引**5**超過了數組的長度,獲取元素失敗,這個時候就會拋出ArrayIndexOfBoundsException
~~~
?Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
? at com.smrobot.exception.demo1.getElement(demo1.java:47)
? at com.smrobot.exception.demo1.main(demo1.java:13)
~~~
由上面可以看出`ArrayIndexOfBoundsException`是一個在代碼運行期間才可能會發生的異常(運行期異常),而讓這個異常產生的原因往往是因為我們自己的原因,例如這個例子中傳入了越界的下標。
#### 異常產生的過程
1. 代碼在main中執行,執行了
~~~
?System.out.println(getElement(nums, 5));
~~~
2. 調用getElement方法,并且傳入越界的下標,由于nums數組的最大長度才為5,找不到為5的索引,因此導致運行時發生的異常。而這個異常JVM認識,因此會將這個異常從該方法中拋出給調用者,即main()方法。
~~~
?public static int getElement(int[] nums, int index) {
? ? ? ? ?return nums[index]; // JVM 會在這里做這么一個操作:throw new ArrayIndexOfBoundsException(5);
?}
~~~
3. main()方法接收到了JVM在getElement方法中拋出的異常對象。由于main()方法也沒有處理該異常的方法,因此為繼續將異常拋出給main()的調用者JVM。而JVM處理該異常的方式就是將異常對象的名稱,內容,異常發生的調用棧打印出來,并且終止程序。

## 三、異常處理
### 相關關鍵字
* try:和catch一起使用,來用包裹可能出現異常的代碼塊;
* catch:和try一起使用,捕獲try中可能出現的異常;
* finally:和try...catch一起使用,不管有沒有異常發生中斷程序的運行,finally代碼塊都會運行;
* throw:用來在方法中拋出一個異常;
* throws:用來聲明方法拋出的異常。
### throw拋出異常
當我們寫一個方法時,為了確保調用者傳遞的參數符合我們的想要的規范,以致于能讓程序健壯運行,這個時候我們就可以使用拋出異常的方式來對傳遞的參數進行提前判斷,如果不符合要求,即可拋出一個異常讓調用者知道。而java中拋出異常的關鍵字就是throw。
使用格式如下:
> throw new 異常類(參數);
舉例:`Objects`類中的非空判斷
~~~
?public static <T> T requireNonNull(T obj) {
? ? ?if (obj == null)
? ? ? throw new NullPointerException();
? ? ?return obj;
?}
~~~
類似的java的源碼中經常可以看到,提前對傳遞的參數進行判斷,增強代碼的健壯性。
當程序執行到了throw語句時,就會停止繼續執行下去,而將對應的異常“返回”給該方法的調用者。(可以將之看成是類似return的效果,即"return"一個異常對象)
那么拋出的異常可以怎么樣處理呢?一種是用throws聲明拋出的異常,讓調用者處理;一種是自己用try...catch處理,而不進行拋出了。
### throws聲明異常
由于拋出的異常一般是將該異常從一個方法中拋出去(main()方法也是一樣的),所以聲明拋出的異常也是寫在方法的定義中,就跟聲明函數的返回值一樣。
使用格式如下:
> 修飾符 返回值類型 方法名(參數) throws 異常類名1,異常類名2…{ }
舉例:
~~~
?public class ThrowsDemo2 {
? ? ?public static void main(String[] args) throws IOException {
? ? ? ? ?read("a.txt");
? ? }
??
? ? ?public static void read(String path) throws FileNotFoundException, IOException {
? ? ? ? ?if (!path.equals("a.txt")) {//如果不是 a.txt這個文件
? ? ? ? ? ? ?// 如果文件名不是a.txt就認為文件不存在
? ? ? ? ? ? ?throw new FileNotFoundException("文件不存在");
? ? ? ? }
? ? ? ? ?if (!path.equals("b.txt")) {
? ? ? ? ? ? ?//文件不為b.txt就拋出IOException,只是舉例用
? ? ? ? ? ? ?throw new IOException();
? ? ? ? }
? ? }
?}
~~~
對于方法中拋出的必檢異常(編譯異常),如果方法內部本身沒有處理,則必須使用throws聲明,提示調用者去處理,不處理在IDEA中就會**標紅**,并且不能編譯通過。而對于運行時期的異常,不進行聲明也是可以的,最后JVM會幫助我們進行中斷處理。
### try...catch捕獲異常
前面的throw和throws并不會真正讓我們自己處理異常發生時要進行的操作,如果我們不處理,最后就是交給JVM中斷程序運行的方式來處理了。try...catch就是可以讓我們捕獲處理異常,不至于讓程序中斷。
使用語法:
~~~
?try {
? //可能出現異常的代碼
?} catch(異常類型1 e) {
? ? ?//處理異常的代碼
? ? ?//常用的有記錄日志,打印異常信息,繼續拋出異常給調用者處理
? ? ?//詳細方法的調用可以看上面的:Throwable中常用的方法
?}
~~~
**捕獲多個異常時的注意事項**
1. 如果在try代碼塊中可能存在多個異常需要捕獲,那么可以使用多個catch進行捕獲,大致的使用方式如下:
~~~
?try {
??
?} catch(異常類型1 e) {
? ? ?
?} catch(異常類型2 e) {
? ? ?
?}...
~~~
注意,在這種處理方式中,如果存在子父類異常,那么需要將子類異常聲明在上面,父類異常聲明在下面。例如,`ArrayIndexOutOfBoundsException`就是`IndexOutOfBoundsException`的子類(具體可查看API或源碼),因此使用上面的方式分別捕獲這兩種異常的時候需要類似于下面的書寫方式
~~~
?try {
??
?} catch(ArrayIndexOutOfBoundsException e) {
? ? ?//子類異常放在上面
?} catch(IndexOutOfBoundsException e) {
? ? ?//父類異常放在下面
?}
~~~
2. 也可以直接使用一個最大的異常對象Exception進行捕獲,這樣就不用寫太多的catch代碼塊了
~~~
?try {
??
?} (Exception e) {
? ? ?//使用異常的基類來捕獲,這樣所有可能發生的異常都會被捕獲到,當然捕獲到是最新發生的異常
?}
~~~
### finally代碼塊
finally代碼塊是和try...catch一起使用,并在放到最后面來使用,常用來關閉各種資源,例如關閉IO資源、數據庫連接資源、鎖資源等等。
finally可以保證不管程序是否出現異常了,相關的資源都可以被關閉掉,因為finally代碼塊是一定會執行的(不轉牛角尖的話!)。
使用語法如下:
~~~
?try {
? ? ?//編寫可能出現異常的代碼
?} catch (Exception e) {
? ? ?//處理異常
?} finally {
? ? ?//關閉已打開的資源
?}
~~~
注意:只要當在try或者catch代碼塊中調用了退出JVM的方法(System.exit(0))時,finally代碼塊才不會執行,不然都會執行的;同時,即使在try或者catch中return語句,finally中的代碼也是會被執行的;如果finally有return語句,那么程序最終返回的是finally代碼塊中return語句的內容。
## 四、自定義異常
我們不僅可以用Java中已經幫我們定義好的異常,也可以自定義屬于自己的異常,來滿足自己的業務需求,例如考試成績是負數的異常、年齡是負數的異常...
自定義異常的方式如下:
1. 如果是自定義編譯期異常,自定義一個類并且繼承Exception;
2. 如果是自定義運行期異常,自定義一個類并且繼承RuntimeException;
舉例如下:
~~~
?// 自己自定義的業務異常 -- 登錄異常
?public class LoginException extends Exception {
? ? ?//給個序列化ID
? ? ?private static final long serialVersionUID = -5116101128118950844L;
? ? ?/**
? ? ? * 空參構造,調用父類空參構造
? ? ?*/
? ? ?public LoginException() {
? ? ? ? ?super();
? ? }
? ? ?/**
? ? ? * 帶參構造,可傳入自定義的提示信息
? ? ?*/
? ? ?public LoginException(String message) {
? ? ? ? ?super(message);
? ? }
?} //可以仿照JDK中各種異常類的定義方法
~~~
自定義的異常的使用方法和使用JDK中的異常方式是一樣的,只不過自定義的異常更加符合自己想要達到的效果罷遼~
## 五、寫在最后

【參考】《Java編程思想》
- 第一章 Java基礎
- ThreadLocal
- Java異常體系
- Java集合框架
- List接口及其實現類
- Queue接口及其實現類
- Set接口及其實現類
- Map接口及其實現類
- JDK1.8新特性
- Lambda表達式
- 常用函數式接口
- stream流
- 面試
- 第二章 Java虛擬機
- 第一節、運行時數據區
- 第二節、垃圾回收
- 第三節、類加載機制
- 第四節、類文件與字節碼指令
- 第五節、語法糖
- 第六節、運行期優化
- 面試常見問題
- 第三章 并發編程
- 第一節、Java中的線程
- 第二節、Java中的鎖
- 第三節、線程池
- 第四節、并發工具類
- AQS
- 第四章 網絡編程
- WebSocket協議
- Netty
- Netty入門
- Netty-自定義協議
- 面試題
- IO
- 網絡IO模型
- 第五章 操作系統
- IO
- 文件系統的相關概念
- Java幾種文件讀寫方式性能對比
- Socket
- 內存管理
- 進程、線程、協程
- IO模型的演化過程
- 第六章 計算機網絡
- 第七章 消息隊列
- RabbitMQ
- 第八章 開發框架
- Spring
- Spring事務
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 數據庫
- Mysql
- Mysql中的索引
- Mysql中的鎖
- 面試常見問題
- Mysql中的日志
- InnoDB存儲引擎
- 事務
- Redis
- redis的數據類型
- redis數據結構
- Redis主從復制
- 哨兵模式
- 面試題
- Spring Boot整合Lettuce+Redisson實現布隆過濾器
- 集群
- Redis網絡IO模型
- 第十章 設計模式
- 設計模式-七大原則
- 設計模式-單例模式
- 設計模式-備忘錄模式
- 設計模式-原型模式
- 設計模式-責任鏈模式
- 設計模式-過濾模式
- 設計模式-觀察者模式
- 設計模式-工廠方法模式
- 設計模式-抽象工廠模式
- 設計模式-代理模式
- 第十一章 后端開發常用工具、庫
- Docker
- Docker安裝Mysql
- 第十二章 中間件
- ZooKeeper