5 异常、断言和日志
title: 異常、斷言和日志
tag: 標(biāo)簽名
categories: 分類
comment: 是否允許評(píng)論(true or false)
description: 描述
top_img: https://z3.ax1x.com/2021/10/06/4xq2s1.png
cover: https://z3.ax1x.com/2021/10/06/4xq2s1.png
處理錯(cuò)誤
假設(shè)一個(gè)Java程序運(yùn)行期間出現(xiàn)了一個(gè)錯(cuò)誤。這個(gè)錯(cuò)誤的原因有很多種,可能是由于文件包含錯(cuò)誤信息,或者網(wǎng)絡(luò)連接出現(xiàn)問題,也有可能是使用了無效的數(shù)組下標(biāo)。用戶期望在出現(xiàn)錯(cuò)誤的時(shí),能夠采取合理的行為。如果由于出現(xiàn)錯(cuò)誤而使得某些操作沒有完成,程序應(yīng)該:
- 返回一種安全的狀態(tài),并能夠讓用戶執(zhí)行其他的命令
- 允許用戶保存所有工作的結(jié)果,并且以妥善的方式終止程序。
異常分類
任何異常對(duì)象都是派生與Throwable類的一個(gè)類實(shí)例。
Error類層次結(jié)構(gòu)描述了Java運(yùn)行時(shí)系統(tǒng)的內(nèi)部錯(cuò)誤和資源耗盡錯(cuò)誤。
在Java程序設(shè)計(jì)中,重點(diǎn)關(guān)注Exception層次結(jié)構(gòu)。它的下面又分為兩個(gè)結(jié)構(gòu),一個(gè)為RuntimeException,另一個(gè)分支為包含其他異常。一般的規(guī)則為:由于編程錯(cuò)誤導(dǎo)致的異常屬于RuntimeException;如果程序本身沒有問題,但是由于像I/O錯(cuò)誤這里問題的異常屬于其他異常。
派生與RuntimeException的異常包括:
- 錯(cuò)誤的強(qiáng)制類型轉(zhuǎn)換
- 數(shù)組范圍越界
- 訪問null指針
不是派生于RuntimeException的異常包括:
- 試圖超越文件末尾繼續(xù)讀取數(shù)據(jù)
- 試圖打開一個(gè)不存在的文件
- 試圖根據(jù)給定的字符串查找Class對(duì)象,而這個(gè)字符串表示的類并不存在。
“如果出現(xiàn)RuntimeException異常,那么就一定是比的問題”,這個(gè)規(guī)則很有道理。應(yīng)該通過檢測(cè)數(shù)組下標(biāo)是否越界來避免ArrayIndexOutOfBoundsException異常;應(yīng)該在使用變量之前通過檢測(cè)它是否為Null來杜絕NullPointException異常的發(fā)生。
Java語(yǔ)法規(guī)范將派生與Error類或RuntimeException類的所有異常稱為非檢查型異常,所有其他的異常稱為檢查型異常。
聲明檢查型異常
如果遇到了無法處理的異常,Java方法可以拋出一個(gè)異常。方法不僅需要告訴編譯器將要返回什么值,還有告訴編譯器有可能發(fā)生什么錯(cuò)誤。如:一段讀取文件的代碼知道有可能讀取的文件不存在,或者文件內(nèi)容為空。
要在方法的首部指出這個(gè)方法可能拋出一個(gè)異常,所以要修改方法首部,以反映這個(gè)方法可能拋出的異常。
public FileInputStream(String name) throws FileNotFoundException這個(gè)聲明表示這個(gè)構(gòu)造器將根據(jù)給定的String參數(shù)產(chǎn)生一個(gè)FileInputStream對(duì)象,但也有可能出錯(cuò)而拋出一個(gè)FileNotFoundException異常。如果出現(xiàn)了這種糟糕的情況,構(gòu)造器將不會(huì)初始化一個(gè)新的FileInputStream對(duì)象,而是拋出一個(gè)FileNotFoundException對(duì)象。如果這個(gè)方法真的拋出了這樣的一個(gè)異常,運(yùn)行時(shí)系統(tǒng)就會(huì)開始搜索如何處理FileNotFoundException對(duì)象的異常處理器。
在自己編寫方法時(shí),不必聲明這個(gè)方法可能拋出的所有異常。在什么時(shí)候需要在方法中用throws子句聲明異常,以及要用throws子句聲明哪些異常,需要記住下面4種情況拋出的異常:
- 調(diào)用一個(gè)拋出檢測(cè)型異常的方法
- 檢測(cè)到一個(gè)錯(cuò)誤,并且利用throw語(yǔ)句拋出一個(gè)檢查型異常
- 程序出現(xiàn)錯(cuò)誤
- Java虛擬機(jī)或運(yùn)行時(shí)庫(kù)出現(xiàn)內(nèi)部錯(cuò)誤。
如果出現(xiàn)前兩種情況,則必須告訴調(diào)用這個(gè)方法的程序員有可能拋出異常。因?yàn)槿魏我粋€(gè)拋出異常得到方法都有可能是一個(gè)死亡陷阱。如果沒有處理器捕獲這個(gè)異常,當(dāng)前的執(zhí)行的線程就會(huì)終止。
有些Java方法包含在對(duì)外提供的類中,對(duì)于這些方法,應(yīng)該通過方法首部的異常規(guī)范聲明這個(gè)方法可能拋出異常。
class MyAnimation{...public Image loadImage(String s)throws IOException{...} }如果一個(gè)方法有可能拋出多個(gè)檢查異常類型,那么就必須在方法得到首部列出所有的異常類。每個(gè)異常類之間用逗號(hào)隔開。
class MyAnimation{...public Image loadImage(String s)throws FileNotFoundException,EOFException{...} }總之,一個(gè)方法必須聲明所有可能拋出的檢查型異常,而非檢查型異常要么在你的控制之外(Error),要么是由從以一開始就應(yīng)該避免的情況所導(dǎo)致的(RuntimeException)。如果你的方法沒有聲明所有可能發(fā)生的檢查型異常,編譯器就會(huì)發(fā)出一個(gè)錯(cuò)誤信息。
如果類中的一個(gè)方法聲明它會(huì)拋出一個(gè)異常,而這個(gè)異常是某個(gè)特定類的實(shí)例,那么這個(gè)方法拋出的異常可能屬于這個(gè)類,也可能屬于這個(gè)類的任意一個(gè)子類。
如何拋出異常
String readData(Scanner in)throws EOFException{...while(...){if(!in.hasNext()) // EOF encounted{if(n < len)throw new EOFException();}...}return s; }如果一個(gè)已有異常的類能夠滿足你的要求,拋出這個(gè)異常非常容易。在這種情況下:
- 找到一個(gè)合適的異常類
- 創(chuàng)建這個(gè)類的一個(gè)對(duì)象
- 將對(duì)象拋出
創(chuàng)建異常類
你的代碼可能會(huì)遇到任何標(biāo)準(zhǔn)異常類都無法描述清楚的問題。在這種情況下,創(chuàng)建自己的異常類就是一件順理成章的事情了。我們需要做的就是定義一個(gè)派生于Exception的類,或者派生于Exception的某個(gè)子類。習(xí)慣做法是,自定義的這個(gè)類應(yīng)該包含兩個(gè)構(gòu)造器,一個(gè)是默認(rèn)的構(gòu)造器,另一個(gè)是包含詳細(xì)描述信息的構(gòu)造器。
class FileFormatException extends IOException{public FileFormatException(){}public FileFormatException(String gripe){super(gripe);} } // 現(xiàn)在,就可以拋出你自定定義的異常類型了。 String readDate(BufferReader in)throws FileFormatException{...while(...){if(ch == -1)// EOF encountered{if(n < len)throw new FileFormatException();}...}return s; }API
Throwable() // 構(gòu)造一個(gè)新的Throwable對(duì)象,但沒有詳細(xì)的描述信息 Throwable(String message) // 構(gòu)造一個(gè)新的Throwable對(duì)象,帶有指定的詳細(xì)描述信息。所有派生的異常類都支持一個(gè)默認(rèn)構(gòu)造器和一個(gè)帶有詳細(xì)描述信息的構(gòu)造器 String getMessage() // 獲得Throwable對(duì)象的詳細(xì)描述信息捕獲異常
捕獲異常
如果發(fā)生了某個(gè)異常,但沒有在任何地方捕獲這個(gè)異常,程序就會(huì)終止,并且在控制臺(tái)上打印一個(gè)消息,其中包括這個(gè)異常的類型和要給堆棧軌跡。
要想捕獲一個(gè)異常,需要設(shè)置try/catch語(yǔ)句塊。最簡(jiǎn)單的try語(yǔ)句塊如下所示:
try{codemore codemore code }catch(ExceptionType e){handler for this type }如果try語(yǔ)句塊種的任何代碼拋出了catch子句種指定的一個(gè)異常類,那么
如果try語(yǔ)句塊中的代碼沒有拋出任何異常,那么程序?qū)⑻^catch子句。
如果方法中的任何代碼拋出了catch子句中沒有聲明的一個(gè)異常類型,那么這個(gè)方法就會(huì)立即退出。
下面是一個(gè)典型的讀取數(shù)據(jù)的代碼:
public void read(String filename){try{var in = new FileInputStream(filename);int b;while((b = in.read())!= -1){process input}}catch (IOException exception){exception.printStackTrace();} } // 讀取并處理字節(jié),直到遇到文件結(jié)束符為止。read方法有可能拋出一個(gè)IOException異常,在這種情況下,將跳出整個(gè)while循環(huán),進(jìn)入catch子句,并生成一個(gè)堆棧軌跡。 // 最好的方式就是什么也不做,而是將異常傳遞給調(diào)用者。如果read方法出現(xiàn)了錯(cuò)誤,就讓read方法的調(diào)用者去操心這個(gè)問題。 public void read(String filename)throws IOException{try{var in = new FileInputStream(filename);int b;while((b = in.read())!= -1){process input}}// 編譯器將嚴(yán)格地執(zhí)行throws說明符。如果調(diào)用了一個(gè)拋出檢查型異常的的方法,就必須處理這個(gè)異常,或者繼續(xù)傳遞這個(gè)異常。請(qǐng)記住,如果編寫一個(gè)方法覆蓋超類的方法,而這個(gè)超類方法沒有拋出異常,你就必須捕獲你的方法代碼中出現(xiàn)的一個(gè)檢查型異常。不允許在子類的throws說明符中出現(xiàn)超類方法未列出的異常類。
捕獲多個(gè)異常
一個(gè)try語(yǔ)句塊中可以捕獲多個(gè)異常類型,并對(duì)不同類型的異常做出不同的處理。要為每個(gè)異常類型使用一個(gè)單獨(dú)的catch子句,如下所示:
try{code that might throw exceptions }catch(FileNotFoundException e){emergency action for missing files }catch(UnkownHostException e){emergency action for unknown hosts }catch(IOException e){emergency action for all other I/O problems }// 同一個(gè)catch子句中可以捕獲多個(gè)異常類型 try{code that might throw exceptions } catch (FileNotFoundException | UnkownHostException e){emergency action for missing files and unkown hosts } catch(IOException e){emergency action for all other I/O problems }再次拋出異常與異常鏈
可以在catch子句中拋出一個(gè)異常。通常,希望改變異常的類型時(shí)會(huì)這樣做。如果開發(fā)了一個(gè)供其他程序使用的子系統(tǒng),可以使用一個(gè)指示子系統(tǒng)故障的異常類型,這很有道理。
try{access the database } catch(SQLException e){throw new ServletException("database error:"+e.getMessage()); } // 不過,可以有一種更好的處理方法,可以把原始異常設(shè)置為新異常的“原因”: try{access the database } catch(SQLException original){var e = new ServletException("database error");e.initCause(Original);throw e; } // 捕獲到這個(gè)異常時(shí),可以使用下面這條語(yǔ)句獲取原始異常 Throwable original = caughtException.getCause();有時(shí)你可能只想記錄一個(gè)異常,再將它重新拋出,而不做任何改變:
try{access the database } catch(Exception e){logger.log(level,message,e);throw e; }fianlly子句
代碼拋出一個(gè)異常,就會(huì)停止處理這個(gè)方法中剩余的代碼,并退出這個(gè)方法。如果這個(gè)方法已經(jīng)獲得了只有它自己知道一些本地資源,而且這些資源必須清理,者就會(huì)有問題。
不管是否有異常被捕獲,finally子句中的代碼都會(huì)執(zhí)行。下面的示例中:
var in = new FileInputStream(...); try{// 1code that might throw exception// 2 } catch(IOException e){// 3show error message// 4 } finally{//5in.close();// 6 }有下列幾種情況執(zhí)行finally子句:
- 代碼沒有拋出異常。在這種情況下,程序首先執(zhí)行try語(yǔ)句塊中的全部代碼,然后執(zhí)行finally子句中的代碼。隨后,繼續(xù)執(zhí)行finally子句之后的第一條語(yǔ)句。執(zhí)行的順序?yàn)?.2.5.6
- 代碼拋出了一個(gè)異常,并在一個(gè)catch子句中捕獲。如果catch子句沒有拋出異常,程序?qū)?zhí)行finally子句之后的第一條語(yǔ)句。這種情況下,執(zhí)行的順序是1.3.4.5.6。如果catch子句拋出了一個(gè)異常,異常將拋回到這個(gè)方法的調(diào)用者。執(zhí)行順序則只是1、3、5。
- 代碼拋出了一個(gè)異常,但沒有任何catch子句捕獲這個(gè)異常。在這種情況下,程序?qū)?zhí)行try語(yǔ)句塊中的所有語(yǔ)句,直到拋出異常為止。這里,執(zhí)行順序只是1、5
try語(yǔ)句可以只有finally子句,而沒有catch子句。
InputStream in = ...; try{code that might throw exceptions }finally{in.close(); } // 無論在try語(yǔ)句塊中是否遇到異常,finally子句中in.close()語(yǔ)句都會(huì)執(zhí)行。 InputStream in = ...; try{try{code that might throw exceptions}finally{in.close();} } catch(IOException e){show error message; } // 內(nèi)嵌try語(yǔ)句只有一個(gè)職責(zé),就是確保關(guān)閉輸入流。外層的try語(yǔ)句塊也只有一個(gè)職責(zé),就是確保報(bào)告出現(xiàn)的錯(cuò)誤。這種解決方案不僅清楚,而且功能更強(qiáng):將會(huì)報(bào)告fianlly子句中出現(xiàn)的錯(cuò)誤。 // 當(dāng)fianlly子句包含return語(yǔ)句時(shí),有可能會(huì)有意向不到的結(jié)果。假設(shè)利用return語(yǔ)句從try語(yǔ)句塊中間退出。在方法返回前,會(huì)執(zhí)行finally子句塊。如果finally塊也有一個(gè)return語(yǔ)句,這個(gè)返回值將會(huì)遮蔽原來的值,如下列例子: public static int parseInt(String s){try{return Integer.parseInt(s);}finally{return 0; // ERROR} } // 這個(gè)方法在正在返回之前會(huì)調(diào)用finlly中的return語(yǔ)句,這樣就會(huì)使得方法最后返回0,而忽略原先的返回值。 // finally子句的體要用于清理資源。不要把改變控制流的語(yǔ)句(return、throw、break、continue)放在finally子句中。try-with-Resources語(yǔ)句
try-with-Resources語(yǔ)句(帶資源的try語(yǔ)句)的最簡(jiǎn)形式為:
try(Resource res = ...){work with res } // try 塊退出是,會(huì)自動(dòng)調(diào)用res.close(). try(var in = new Scanner(new FileInputStream("/usr/share/dict/words"),StandardCharsets.UTF_8)){while(in.hahsNext()){System.out.println(in.next());} } // 這個(gè)塊正常退出時(shí),或者存在一個(gè)異常時(shí),都會(huì)調(diào)用in.close()方法,就好像使用了fianlly塊一樣。在Java9中,可以在try首部中提供之前聲明的事實(shí)最終變量:
public static void printAll(String[] lines,PrintWriter out){try(out){ // effectively final variablefor(String line: lines){out.println(line);}}// out.close() }分析堆棧軌跡元素
堆棧軌跡元素是程序執(zhí)行過程中某個(gè)特定點(diǎn)上所有掛起的方法調(diào)用一個(gè)列表。當(dāng)Java程序因?yàn)橐粋€(gè)未捕獲的異常而終止時(shí),就會(huì)顯示堆棧軌跡。
可以調(diào)用Throwable類的printStackTrace方法訪問堆棧軌跡的文本描述信息。
var t = new Throwable(); var out = new StringWriter(); t.printStackTrace(new PrintWriter(out)); String description = out.toString();使用異常的技巧
-
異常處理不能代替簡(jiǎn)單的測(cè)試
-
不要過分地細(xì)化異常
PrintStream out; Stack s; for(i = 0; i < 100; i++){try{n = s.pop();}catch(EmptyStackException e){// stack was empty}try{out.writeInt(n);}catch(IOException e){// problem writing to file} } // 這種編碼方式會(huì)使得代碼量急劇膨脹 // 正確的做法如下: try{for(int i = 0;i < 100; i++){n = s.pop();out.writeInt(n);} }catch(IOException e){// problem writing to file }catch(EmptyStackException e){// stack was empty } -
充分利用異常層次結(jié)構(gòu)
不要只拋出RuntimeException異常。應(yīng)該尋找一個(gè)適合的子類或創(chuàng)建自己的異常類
不要只捕獲Throw異常,否則,這會(huì)使你的代碼更加難讀,更加難維護(hù)。
-
不要壓制異常
-
在檢測(cè)錯(cuò)誤時(shí),"苛刻"要比放任更好。
-
不要羞于傳遞異常
很多程序員都感覺應(yīng)該捕獲拋出的全部異常。如果調(diào)用了一個(gè)拋出異常的方法,例如,FileInputStream構(gòu)造器或readLine方法,它們會(huì)本能地捕獲這些可能產(chǎn)生的異常。其實(shí),最好繼續(xù)傳遞這個(gè)異常,而不是自己捕獲:
public void readStuff(String filename)throws IOException {var in = new FileInputStream(filename,StandardCharsets.UTF_8);... }使用斷言
斷言的概念
Java語(yǔ)言引入了關(guān)鍵字assert。這個(gè)關(guān)鍵字有兩種形式:
assert condition; 和 assert condition:expression;
這兩個(gè)語(yǔ)句都會(huì)計(jì)算條件,如果結(jié)果為false,則拋出一個(gè)AssertionError異常。在第二個(gè)語(yǔ)句中,表達(dá)式將傳入AssertionError對(duì)象的構(gòu)造器,并轉(zhuǎn)換成一個(gè)消息字符串。
要想斷言x是一個(gè)非負(fù)數(shù),只需要簡(jiǎn)單地使用下面這條語(yǔ)句
assert x >= 0; // 或者將x的實(shí)際值傳遞給AssertionError對(duì)象,以便以后顯示啟用和禁用斷言
在默認(rèn)情況下,斷言是禁用的。可以在運(yùn)行程序時(shí)用-enableassertions或-ea選項(xiàng)啟用斷言:
也可以在某個(gè)類或整個(gè)包中啟用斷言,例如
java -ea:MyClass -ea:com.mycompany.mylib MyApp // 這條命令將為MyClass類以及com.mycompany.mylib包和它的子包中的所有類打開斷言。使用斷言完成參數(shù)檢查
什么時(shí)候應(yīng)該選擇使用斷言?應(yīng)該記住下面幾點(diǎn):
- 斷言失敗時(shí)致命的、不可恢復(fù)的錯(cuò)誤。
- 斷言檢查只是在開發(fā)和測(cè)試階段打開。
日志
使用日志API的優(yōu)點(diǎn):
- 可以容易地取消全部日志記錄,或者僅僅取消某個(gè)級(jí)別以下的日志,而且可以很容易地再次打開日志開關(guān)
- 可以很簡(jiǎn)單地禁止日志記錄,因此,將這些日志代碼留在程序中的開銷很小。
基本日志
要生成簡(jiǎn)單的日志記錄,可以使用全局日志記錄器并調(diào)用其info方法:
Logger.getGobal().info("File -> Open menu item selected");高級(jí)日志
在一個(gè)專業(yè)的應(yīng)用程序中,你可以定義自己的日志記錄器,可以調(diào)用getLogger方法創(chuàng)建或獲取日志記錄器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");通常,有以下7個(gè)日志級(jí)別:
- SEVERE
- WARING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
在默認(rèn)情況下,實(shí)際上只記錄前3個(gè)級(jí)別。也可以設(shè)置一個(gè)不同的級(jí)別,如
logger.setLevel(Level.FINE); // 現(xiàn)在,FINE以及所有更高級(jí)別的日志都會(huì)記錄使用Log4j
Log4j是一種非常流行的日志框架,最新版本是2.x
Log4j是一個(gè)組件化設(shè)計(jì)的日志系統(tǒng),它的架構(gòu)大致如下:
當(dāng)我們使用Log4j輸出一條日志時(shí),Log4j自動(dòng)通過不同的Appender把同一條日志輸出到不同的目的地。如
- console:輸出到屏幕
- file:輸出到文件
- socket:通過網(wǎng)絡(luò)輸出到遠(yuǎn)程計(jì)算機(jī)
- jdbc:輸出到數(shù)據(jù)庫(kù)
在輸出日志的過程中,通過Filter來過濾哪些log需要被輸出,哪些log不需要被輸出。例如,僅輸出ERROR級(jí)別的日志。最后,通過Layout來格式化日志信息,如,自動(dòng)添加日期、時(shí)間、方法名稱等信息。
我們?cè)趯?shí)際使用的時(shí)候,并不需要關(guān)心Log4j的API,而是通過配置文件類配置它。
以XML為例:
<?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><!-- 根據(jù)文件大小自動(dòng)切割日志 --><SizeBasedTriggeringPolicy size="1 MB" /></Policies><!-- 保留最近10份 --><DefaultRolloverStrategy max="10" /></RollingFile></Appenders><Loggers><Root level="info"><!-- 對(duì)info級(jí)別的日志,輸出到console --><AppenderRef ref="console" level="info" /><!-- 對(duì)error級(jí)別的日志,輸出到err,即上面定義的RollingFile --><AppenderRef ref="err" level="error" /></Root></Loggers> </Configuration>雖然配置Log4j比較繁瑣,但一旦配置完成,使用起來就非常方便。對(duì)上面的配置文件,凡是INFO級(jí)別的日志,會(huì)自動(dòng)輸出到屏幕,而ERROR級(jí)別的日志,不但會(huì)輸出到屏幕,還會(huì)同時(shí)輸出到文件,并且,一旦日志文件達(dá)到指定大小,Log4j就會(huì)自動(dòng)切割新的日志文件,并且最多保留10份。
割日志 -->
總結(jié)
以上是生活随笔為你收集整理的5 异常、断言和日志的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java中display1_关于disp
- 下一篇: Scrapy抓取起点中文网排行榜