Java的基本使用之異常處理
1、Java中異常的基本概念
異常是一種類,因此它本身帶有類型信息。異常可以在任何地方拋出,但只需要在上層捕獲。
在 Java 中拋出異常的目的是為了在代碼執行發生錯誤的時候,停止,或者進行處理,以及拋出信息幫助程序員定位出現bug的位置。所以,我們需要在可能發生異常的地方拋出異常并進行捕獲處理。
Java 中異常的繼承關系如下:

由上圖可知,Throwable是異常體系的根,它繼承自Object。
所有異常都可以調用 e.printStackTrace() 方法進行簡單打印輸出。
1.1、Error 和 Exception
Throwable有兩個體系:Error和Exception。
1.1.1、Error
Error表示嚴重的錯誤,程序對此一般無能為力,例如:
OutOfMemoryError:內存耗盡NoClassDefFoundError:無法加載某個ClassStackOverflowError:棧溢出
1.1.2、Exception
而Exception則是運行時的錯誤,它可以被捕獲并處理。
Exception分為兩大類:
RuntimeException以及它的子類;- 非
RuntimeException(包括IOException、ReflectiveOperationException等等)
1.3、哪些異常需要被捕獲
有些異常是應用程序邏輯處理的一部分,應該捕獲并處理。例如:
NumberFormatException:數值類型的格式錯誤FileNotFoundException:未找到文件SocketException:讀取網絡失敗
還有一些異常是程序邏輯編寫不對造成的,應該修復程序本身。例如:
NullPointerException:對某個null的對象調用方法或字段。空指針異常,如果一個對象為null,調用其方法或訪問其字段就會產生NullPointerException。指針這個概念實際上源自C語言,Java語言中并無指針。我們定義的變量實際上是引用,Null Pointer更確切地說是Null Reference,IndexOutOfBoundsException:數組索引越界
Java規定:
-
必須捕獲的異常:包括
Exception及其子類,但不包括RuntimeException及其子類,這種類型的異常稱為Checked Exception。 -
不需要捕獲的異常:包括
Error及其子類,RuntimeException及其子類。
2、捕獲異常(try{...} catch(){...})
在Java中,凡是可能拋出異常的語句,都可以用try ... catch進行捕獲。通過捕獲異常就可以針對異常情況做操作,并且避免后面的程序被中斷。
只要是在方法拋出的 Checked Exception,如果不在該方法的調用層捕獲,那就應該拋出來,然后在更高的調用層捕獲。所有未捕獲的異常,最終也必須在main()方法中捕獲或者在 main 方法中拋出然后由 JVM 進行處理,否則會編譯報錯。main()方法是最后捕獲異常的機會,不推薦在 main 方法中拋出異常。
在捕獲了異常后我們應該進行相應的操作,至少應該打印記錄異常。
public static void main(String[] args) { try { ... } catch (Exception e) { e.printStackTrace(); } }
我們可以使用多個 catch 語句,每個catch分別捕獲對應的Exception及其子類。JVM在捕獲到異常后,會從上到下匹配catch語句,匹配到某個catch后,執行catch代碼塊,然后不再繼續匹配。簡單地說就是多個catch語句只有一個能被執行。
存在多個catch的時候,子類必須寫在前面,否則永遠捕獲不到該子類異常。
public static void main(String[] args) { try { process1(); } catch (UnsupportedEncodingException e) { //子類應該寫在前面,否則永遠捕獲不到 System.out.println("Bad encoding"); } catch (IOException | NumberFormatException e) { //處理不同異常的代碼一樣時,我們可以用 | 把它們寫在一起 System.out.println("Bad input"); } }
2.1、finally 語句
Java 的try ... catch機制還提供了finally語句,finally語句塊保證有無錯誤都會執行。
public static void main(String[] args) { try { ... } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("END"); } }
finally 語句不是必須的,可寫可不寫,并且它總是最后執行的,finally 語句是用來保證一些代碼必須執行的。
你也可以只使用try ... finally,而不寫catch語句,例如:
//下面方法聲明了可能拋出的異常,所以可以不寫catch。 void process(String file) throws IOException { try { ... } finally { System.out.println("END"); } }
在catch語句塊中拋出異常,finally語句仍然會執行:
public class Main { public static void main(String[] args) { try { Integer.parseInt("abc"); } catch (Exception e) { System.out.println("catched"); throw new RuntimeException(e); } finally { System.out.println("finally"); } } } //上述代碼執行結果如下: catched finally Exception in thread "main" java.lang.RuntimeException: java.lang.NumberFormatException: For input string: "abc" at Main.main(Main.java:8) Caused by: java.lang.NumberFormatException: For input string: "abc" at ...
先打印了catched,后打印finally,然后再輸出異常信息。說明先執行了catch語句,然后又執行了finally語句塊,最后才拋出了異常并且在 JVM 中捕獲。因此,在catch中拋出異常,不會影響finally的執行。JVM會先執行finally,然后才拋出異常。
3、拋出異常
當某個方法拋出了異常時,如果在調用該方法時沒有捕獲異常,那就應該拋出來,由更上層來進行捕獲,以此類推,直到遇到某個try ... catch被捕獲為止。所有未捕獲的異常,最終也必須在main()方法中捕獲或者在 main 方法中拋出然后由 JVM 進行處理,否則會編譯報錯。
3.1、如何拋出異常
拋出異常分兩步:
- 創建某個
Exception的實例; - 用
throw語句拋出。
代碼示例:
public void process2(String s) { if (s==null) { NullPointerException e = new NullPointerException("異常信息"); throw e; //一般寫成一行即可 throw new NullPointerException("異常信息"); } }
子類重寫父類的方法時,子類方法不能拋出比父類方法更大的異常。
3.2、異常轉換
如果一個方法使用 catch 捕獲了某個異常后,又在 catch 語句中拋出新的異常,此時就相當于把拋出的異常類型“轉換”了,此時如果在其他方法中捕獲新異常,舊異常的信息不會被捕獲到。
public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { //此時輸出process1方法的異常,并不會輸出process2方法中的異常信息 e.printStackTrace(); } } static void process1() { try { process2(); } catch (NullPointerException e) { //捕獲process2方法的異常并轉換 throw new IllegalArgumentException(); } } static void process2() { throw new NullPointerException(); } }
這樣新的異常就會丟失原始異常信息,我們無法看不到原始異常NullPointerException的信息。
為了能追蹤到完整的異常棧,在構造異常的時候,把原始的Exception實例傳進去,新的Exception就可以持有原始Exception信息。對上述代碼改進如下:
public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(e); //這里將原始的異常傳了進去 } } static void process2() { throw new NullPointerException(); } } //運行上述代碼,打印出的異常棧類似以下信息: //Caused by: Xxx,說明捕獲的IllegalArgumentException并不是造成問題的根源,根源在于NullPointerException,是在Main.process2()方法拋出的。 java.lang.IllegalArgumentException: java.lang.NullPointerException at Main.process1(Main.java:15) at Main.main(Main.java:5) Caused by: java.lang.NullPointerException at Main.process2(Main.java:20) at Main.process1(Main.java:13)
在代碼中獲取原始異常可以使用Throwable.getCause()方法。如果返回null,說明已經是“根異常”了。
有了完整的異常棧的信息,我們才能快速定位并修復代碼的問題。
4、打印異常信息(e.printStackTrace())
所有異常都可以通過printStackTrace()打印出方法的調用棧,打印信息類似下面:
java.lang.NumberFormatException: null at java.base/java.lang.Integer.parseInt(Integer.java:614) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Main.process2(Main.java:16) at Main.process1(Main.java:12) at Main.main(Main.java:5)
上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被拋出的,從下往上看,調用層次依次是:
main()調用process1();process1()調用process2();process2()調用Integer.parseInt(String);Integer.parseInt(String)調用Integer.parseInt(String, int)。
每層調用都給出了源代碼的行號,可直接定位。
5、使用日志(JDK Logging)
日志就是Logging,它的目的是為了取代System.out.println()。
5.1、使用日志的好處
- 可以設置輸出樣式,避免自己每次都寫
"ERROR: " + var - 可以設置輸出級別,禁止某些級別輸出。例如,只輸出錯誤日志
- 可以被重定向到文件,存檔,這樣可以在程序運行結束后查看日志,便于追蹤問題
- 可以根據配置文件調整日志,無需修改代碼
- 可以按包名控制日志級別,只輸出某些包打的日志等等。。。
5.2、如何使用日志
Java 標準庫內置了日志包java.util.logging,我們可以直接用。
import java.util.logging.Level; import java.util.logging.Logger; public class Hello { public static void main(String[] args) { Logger logger = Logger.getGlobal(); logger.info("start process..."); logger.warning("memory is running out..."); logger.fine("ignored."); logger.severe("process will be terminated..."); } } //打印以下信息: Mar 02, 2019 6:32:13 PM.Hello main //自動打印出時間、調用類、調用方法等很多有用的信息。 INFO: start process... Mar 02, 2019 6:32:13 PM.Hello main WARNING: memory is running out... Mar 02, 2019 6:32:13 PM.Hello main SEVERE: process will be terminated...
上面的輸出當中,logger.fine()沒有打印。這是因為,日志的輸出可以設定級別。JDK 的 Logging 定義了7個日志級別,從嚴重到普通。默認級別是 INFO,INFO 級別以下的日志,不會被打印出來。使用日志級別的好處在于,調整級別,就可以屏蔽掉很多調試相關的日志輸出。
日志級別如下:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
Java標準庫內置的Logging有以下局限:Logging系統在JVM啟動時讀取配置文件并完成初始化,一旦開始運行main()方法,就無法修改配置;配置不太方便,需要在JVM啟動時傳遞參數-Djava.util.logging.config.file=<config-file-name>。因此,Java標準庫內置的Logging使用并不是非常廣泛。
6、使用 Commons Logging
和Java標準庫提供的日志不同,Commons Logging是一個第三方日志庫,它可以掛接不同的日志系統,并通過配置文件指定掛接的日志系統。默認情況下,Commons Loggin自動搜索并使用Log4j,如果沒有找到Log4j,再使用JDK Logging。
Commons Logging是一個第三方提供的庫,要想使用必須先把它下載下來,鏈接:https://commons.apache.org/proper/commons-logging/download_logging.cgi
下載后,解壓,找到commons-logging-1.2.jar這個文件,將 jar 文件放在項目根目錄下,并且在 Libraries 中引入該 jar 文件,然后即可使用
//先通過LogFactory獲取Log類的實例,然后使用Log實例的方法打日志 import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class Main { public static void main(String[] args) { Log log = LogFactory.getLog(Main.class); log.info("start..."); log.warn("end."); } }
Commons Logging定義了6個日志級別,默認級別是INFO,即INFO級別以下的都不會打印出來
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
7、使用 Log4j
Log4j是一種非常流行的日志框架,Commons Logging可以作為“日志接口”來使用,而真正的“日志實現”可以使用Log4j。
當我們使用 Log4j 輸出一條日志時,Log4j 自動通過不同的 Appender 把同一條日志輸出到不同的目的地
使用Log4j的時候我們可以通過配置文件來讓 Log4j 讀取配置文件并按照我們的配置來輸出日志,例如:
<?xml version="1.0" encoding="UTF-8"?> <Configuration> <Properties> <!-- 定義日志格式 --> <Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property> <!-- 定義文件名變量 --> <Property name="file.err.filename">log/err.log</Property> <Property name="file.err.pattern">log/err.%i.log.gz</Property> </Properties>
<!-- 定義Appender,即目的地 --> <Appenders> <!-- 定義輸出到屏幕 --> <Console name="console" target="SYSTEM_OUT"> <!-- 日志格式引用上面定義的log.pattern --> <PatternLayout pattern="${log.pattern}" /> </Console> <!-- 定義輸出到文件,文件名引用上面定義的file.err.filename --> <RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}"> <PatternLayout pattern="${log.pattern}" /> <Policies> <!-- 根據文件大小自動切割日志 --> <SizeBasedTriggeringPolicy size="1 MB" /> </Policies> <!-- 保留最近10份 --> <DefaultRolloverStrategy max="10" /> </RollingFile> </Appenders>
<Loggers> <Root level="info"> <!-- 對info級別的日志,輸出到console --> <AppenderRef ref="console" level="info" /> <!-- 對error級別的日志,輸出到err,即上面定義的RollingFile --> <AppenderRef ref="err" level="error" /> </Root> </Loggers> </Configuration>
有了配置文件還不夠,因為Log4j也是一個第三方庫,我們需要從這里(https://www.liaoxuefeng.com/wiki/1252599548343744/1264739436350112)下載Log4j,解壓后,把以下3個jar包添加至項目依賴中
- log4j-api-2.x.jar
- log4j-core-2.x.jar
- log4j-jcl-2.x.jar
把Commons Logging 添加至項目依賴中,Commons Logging會自動發現并使用Log4j。要打印日志,只需要按Commons Logging的寫法寫即可,不需要改動任何代碼,就可以得到Log4j的日志輸出。
在開發階段,始終使用Commons Logging接口來寫入日志,并且開發階段無需引入Log4j。如果需要把日志寫入文件, 只需要把正確的配置文件和Log4j相關的jar包放入classpath,就可以自動把日志切換成使用Log4j寫入,不需要修改任何代碼。
參考:https://www.liaoxuefeng.com/wiki/1252599548343744/1264739436350112

浙公網安備 33010602011771號