《Java核心技術 卷Ⅰ》 第7章 異常、斷言和日誌前端
若是因爲出現錯誤而是的某些操做沒有完成,程序應該:java
檢測(或引起)錯誤條件的代碼一般離:git
的代碼很遠。github
異常處理的任務:將控制權從錯誤產生地方轉移給可以處理這種狀況的錯誤處理器。數據庫
在Java中,異常對象都是派生於Throwable
類的一個實例,若是Java中內置的異常類不能知足需求,用戶還能夠建立本身的異常類。編程
Java異常層次結構:數組
Throwable
Error
Exception
IOException
RuntimeException
能夠看到第二層只有Error
和Exception
。安全
Error
類層次結構描述了Java運行時系統的內部錯誤和資源耗盡錯誤,應用程序不該該拋出這種類型的對象,這種內部錯誤的狀況不多出現,出現了能作的工做也不多。app
設計Java程序時,需關注Exception
層次結構,這個層次又分爲兩個分支,RuntimeException
和包含其餘異常的IOException
。測試
劃分兩個分支的規則是:
RuntimeException
IOException
派生於RuntimeException
的異常包含下面幾種狀況:
派生於IOException
的異常包含下面幾種狀況:
Class
對象,而這個字符串表示的類並不存在Java語言規範將派生於Exception
類和RuntimeException
類的全部異常統稱非受查(unchecked)異常,全部其餘異常都是受查(checked)異常。
編譯器將覈查是否爲全部的受查異常提供了異常處理器。
一個方法不只要告訴編譯器將要返回什麼值,還要告訴編譯器有可能發生什麼錯誤。
異常規範(exception specification):方法應該在其首部聲明所可能拋出的異常。
public FileInputStream(String name) throws FileNotFoundException 複製代碼
若是這個方法拋出了這樣的異常對象,運行時系統會開始搜索異常處理器,以便知道如何處理這個異常對象。
固然不是全部方法都須要聲明異常,下面4種狀況應該拋出異常:
throw
語句拋出一個受查異常出現前兩種狀況之一,就必須告訴調用者這個方法可能的異常,由於若是沒有處理器捕獲這個異常,當前執行的線程就會結束。
若是一個方法有多個受查異常類型,就必須在首部列出全部的異常類,異常類之間用逗號隔開:
class MyAnimation {
...
public Image loadImage(String s) throws FileNotFoundException, EOFException {
...
}
}
複製代碼
可是不須要聲明Java的內部錯誤,即從Error
繼承的錯誤。
關於子類和超類在這部分的問題:
若是類中的一個方法聲明拋出一個異常,而這個異常是某個特定類的實例時:
IOExcetion
)FileNotFoundException
)假設程序代碼中發生了一些很糟糕的事情。
首先要決定應該拋出什麼類型的異常(經過查閱已有異常類的Java API文檔)。
拋出異常的語句是:
throw new EOFException();
// 或者
EOFException e = new EOFException();
throw e;
複製代碼
一個名爲readData
的方法正在讀取一個首部有信息Content-length: 1024
的文件,然而讀到733個字符以後文件就結束了,這是一個不正常的狀況,但願拋出一個異常。
String readData(Scanner in) throws EOFException {
...
while(...)
{
if(!in.hasNext()) // EOF encountered
{
if(n < len)
throw new EOFException();
}
...
}
return s;
}
複製代碼
EOFException
類還有一個含有一個字符串類型參數的構造器,這個構造器能夠更加細緻的描述異常出現的狀況。
String gripe = "Content-length:" + len + ", Received:" + n;
throw new EOFException(gripe);
複製代碼
對於一個已經存在的異常類,將其拋出比較容易:
一旦拋出異常,這個方法就不可能返回到調用者,即沒必要爲返回的默認值或錯誤代碼擔心。
實際狀況中,可能會遇到任何標準異常類不能充分描述的問題,這時候就應該建立本身的異常類。
須要作的只是定義一個派生於Exception
的類,或者派生於Exception
子類的類。
習慣上,定義的類應該包含兩個構造器:
Throwable
的toString
方法會打印出這些信息,在調試中有不少用)class FileFormatException extends IOException {
public FileFormatException() {}
public FileFormatException(String gripe) {
super(gripe);
}
}
複製代碼
要想捕獲一個異常,必須設置try
/catch
語句塊。
try
{
code
...
}
catch(ExceptionType e)
{
handler for this type
}
複製代碼
若是try
語句塊中任何代碼拋出了一個在catch
子句中說明的異常類,那麼:
try
語句塊的其他代碼catch
子句中的處理器代碼若是沒有代碼拋出任何異常,程序跳過catch
子句。
若是方法中的任何代碼拋出了一個在catch
子句中沒有聲明的異常類型,那麼這個方法就會當即退出。
// 讀取數據的典型代碼
public void read(String filename) {
try
{
InputStream in = new FileInputStream(filename);
int b;
while((b = in.read()) != -1)
{
// process input
...
}
}
catch(IOException exception)
{
exception.printStackTrace();
}
}
複製代碼
read
方法有可能拋出一個IOException
異常,這種狀況下,將跳出整個while
循環,進入catch
子句,並生成一個棧軌跡。
還有一種選擇就是什麼也不作,而是將異常傳遞給調用者。
public void read(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
int b;
while((b = in.read()) != -1)
{
// process input
...
}
}
複製代碼
編譯器嚴格地執行throws
說明符,若是調用了一個拋出受查異常的方法,就必須對它進行處理,或者繼續傳遞。
兩種方式哪一種更好?
一般,應該捕獲那些知道如何處理的異常,將那些不知道怎麼樣處理的異常進行傳遞。
這個規則也有一個例外:若是編寫一個覆蓋超類的方法,而這個方法又沒有拋出異常,那麼這個方法就必須捕獲方法代碼中出現的每個受查異常;而且不容許在子類的throws
說明符中出現超過超類方法所列出的異常類範圍。
爲每一個異常類型使用一個單獨的catch
子句:
try
{
code
...
}
catch(FileNotFoundException e)
{
handler for missing files
}
catch(UnknownHostException e)
{
handler for unknown hosts
}
catch(IOException e)
{
handler for all other I/O problems
}
複製代碼
異常對象可能包含與異常相關的信息,可使用e.getMessage()
得到詳細的錯誤信息,或者使用e.getClass().getName()
獲得異常對象的實際類型。
在Java SE 7中,同一個catch
子句中能夠捕獲多個異常類型,若是動做同樣,能夠合併catch
子句:
try
{
code
...
}
catch(FileNotFoundException | UnknownHostException e)
{
handler for missing files and unknown hosts
}
catch(IOException e)
{
handler for all other I/O problems
}
複製代碼
只有當捕獲的異常類型彼此之間不存在子類關係時才須要這個特性。
在catch
子句中能夠拋出一個異常,這樣作的目的是改變異常的類型。
try
{
access the database
}
catch(SQLException e)
{
throws new ServletException("database error:" + e.getMessage());
}
複製代碼
ServletException
用帶有異常信息文本的構造器來構造。
不過還有一種更好的處理方法,並將原始異常設置爲新異常的「緣由」:
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
複製代碼
當捕獲到異常時,可使用下面這條語句從新獲得原始異常:
Throwable e = se.getCause();
複製代碼
這樣可讓用戶拋出子系統中的高級異常,而不會丟失原始異常的細節。
當代碼拋出一個異常時,就會終止方法中剩餘代碼的處理,並退出這個方法的執行。
若是方法得到了一些本地資源,而且只有這個方法本身知道,又若是這些資源在退出方法以前必須被回收(好比數據庫鏈接的關閉),那麼就會產生資源回收問題。
一種是捕獲並從新拋出全部異常,這種須要在兩個地方清除所分配的資源,一個在正常代碼中,另外一個在異常代碼中。
Java有一種更好地解決方案,就是finally
子句。
無論是否有異常被捕獲,finally
子句的代碼都會被執行。
InputStream in = new FileInputStream(...);
try
{
// 1
code that might throw exception
// 2
}
catch(IOException e)
{
// 3
show error message
// 4
}
finally
{
// 5
in.close();
}
// 6
複製代碼
上面的代碼中,有3種狀況會執行finally
子句:
catch
子句中捕獲的異常
catch
子句沒有拋出異常,執行序列爲一、三、四、五、6catch
子句拋出一個異常,異常將被拋回這個方法的調用者,執行序列爲一、三、5(注意沒有6)try
語句能夠只有finally
子句,沒有catch
子句。
有時候finally
子句也會帶來麻煩,好比清理資源時也可能拋出異常。
若是在try
中發生了異常,而且被catch
捕獲了異常,而後在finally
中進行處理資源時若是又發生了異常,那麼原有的異常將會丟失,轉而拋出finally
中處理的異常。
這個時候的一種解決辦法是用局部變量Exception ex
暫存catch
中的異常:
try
中進行執行的時候加入嵌套的try/catch
,並在catch
中暫存ex
並向上拋出finally
中處理資源的時候加入嵌套的try/catch
,而且在catch
中進行判斷ex
是否存在來進一步處理InputStream in = ...;
Exception ex = null;
try
{
try
{
code that might throw exception
}
catch(Exception e)
{
ex = e;
throw e;
}
}
finally
{
try
{
in.close();
}
catch(Exception e)
{
if(ex == null)throw e;
}
}
複製代碼
下一節會介紹,Java SE 7中關閉資源的處理會容易不少。
對於如下代碼模式:
open a resource
try
{
work with the resource
}
finally
{
close the resource
}
複製代碼
假設資源屬於一個實現了AutoCloseable
接口的類,Java SE 7位這種代碼提供了一個頗有用的快捷方式,AutoCloseable
接口有一個方法:
void close() throws Exception 複製代碼
帶資源的try
語句的最簡形式爲:
try(Resource res = ...)
{
work with res
}
複製代碼
try
塊退出時,會自動調用res.close()
。
try(Scanner in = new Scanner(new FileInputStream("..."), "UTF-8"))
{
while(in.hasNext())
System.out.println(in.next());
}
複製代碼
這個塊正常退出或存在一個異常時,都會調用in.close()
方法,就好像使用了finally
塊同樣。
還能夠指定多個資源:
try(Scanner in = new Scanner(new FileInputStream("..."), "UTF-8");
PrintWriter out = new PrintWriter("..."))
{
while(in.hasNext())
System.out.println(in.next().toUpperCase());
}
複製代碼
不論如何這個塊如何退出,in
和out
都會關閉,可是若是用常規手動編程,就須要兩個嵌套的try/finally
語句。
以前的close
拋出異常會帶來難題,而帶資源的try
語句能夠很好的處理這種狀況,原來的異常會被從新拋出,而close
方法帶來的異常會「被抑制」。
堆棧軌跡(stack trace)是一個方法調用過程的列表,包含了程序執行過程當中方法調用的特定位置。
能夠調用Throwable
類的printStackTrace
方法訪問堆棧軌跡的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
複製代碼
一種更靈活的方法是使用getStackTrace
方法,會獲得StackTraceElement
對象的一個數組,能夠在程序中分析這個對象數組:
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
analyze frame
複製代碼
StackTraceElement
類含有可以得到文件名和當前執行的代碼行號的方法,同時還含有能得到類名和方法名的方法,toString
方法會產生一個格式化的字符串,其中包含所得到的信息。
靜態的Thread.getAllStackTraces
方法,能夠產生全部線程的堆棧軌跡。
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
StackTraceElememt[] frames = map.get(t);
analyze frames
}
複製代碼
Throwable(Throwable cause)
Throwable(String message, Throwable cause)
Throwable initCause(Throwable cause)
:將這個對象設置爲「緣由」,若是這個對象已經被設置爲「緣由」,則拋出一個異常,返回this
引用。Throwable getCause()
:得到設置爲這個對象的「緣由」的異常對象,若是沒有則爲null
StackTraceElement[] getStackTrace()
:得到構造這個對象時調用堆棧的跟蹤void addSuppressed(Throwable t)
:爲這個異常增長一個抑制異常Throwable[] getSuppressed()
:獲得這個異常的全部抑制異常String getFileName()
int getLineNumber()
String getClassName()
String getMethodName()
boolean isNativeMethod()
:若是這個元素運行時在一個本地方法中,則返回true
String toString()
:若是存在的話,返回一個包含類名、方法名、文件名和行數的格式化字符串,如StackTraceTest.factorial(StackTraceTest.java:18)
1.異常處理不能代替簡單的測試。
在進行一些風險操做時(好比出棧操做),應該先檢測當前操做是否有風險(好比檢查是否已經空棧),而不是用異常捕獲來代替這個測試。
與簡單的測試相比,捕獲異常須要花費更多的時間,因此:只在異常狀況下使用異常機制。
2.不要過度細分化異常。
若是能夠寫成一個try/catch(s)
的語句,那就不要寫成多個try/catch
。
3.利用異常層次結構。
不要只拋出RuntimeException
異常,應該尋找更適合的子類或建立本身的異常類。
不要只拋出Throwable
異常,不然會使程序代碼可讀性、可維護性降低。
4.不要壓制異常。
在Java中,傾向於關閉異常。
public Image loadImage(String s) {
try
{
codes
}
catch(Exception e)
{}
}
複製代碼
這樣代碼就能夠經過編譯了,若是發生了異常就會被忽略。固然若是認爲異常很是重要,就應該對它們進行處理。
5.檢測錯誤時,「苛刻」要比聽任更好。
6.不要羞於傳遞異常。
有時候傳遞異常比捕獲異常更好,讓高層次的方法通知用戶發生了錯誤,或者放棄不成功的命令更加適宜。
這部分和測試相關,之後有須要的話單獨開設一章進行說明。
不要再使用System.out.println
來進行記錄了!
使用記錄日誌API吧!
簡單的日誌記錄,可使用全局日誌記錄器(global logger)並調用info
方法:
Logger.getGlobal().info("File->Open menu item selected");
複製代碼
默認狀況下會顯示:
May 10, 2013 10:12:15 ....
INFO: File->Open menu item selected
複製代碼
若是在適當的地方調用:
Logger.getGlobal().setLevel(Level.OFF);
複製代碼
能夠不用將全部的日誌都記錄到一個全局日誌記錄器中,也能夠自定義日誌記錄器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
複製代碼
未被任何變量引用的日誌記錄器可能會被垃圾回收,爲了不這種狀況,能夠用一個靜態變量存儲日誌記錄器的一個引用。
與包名相似,日誌記錄器名也具備層次結構,而且層次性更強。
對於包來講,包的名字與其父包沒有語義關係,可是日誌記錄器的父與子之間共享某些屬性。
例如,若是對com.mycompany
日誌記錄器設置了日誌級別,它的子記錄器也會繼承這個級別。
一般有如下7個日誌記錄器級別Level
:
SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST
默認狀況下,只記錄前三個級別。
另外,可使用Level.ALL
開啓全部級別的記錄,或者使用Level.OFF
關閉全部級別的記錄。
對於全部的級別有下面幾種記錄方法:
logger.warning(message);
logger.info(message);
複製代碼
也可使用log方法指定級別:
logger.log(Level.FINE, message);
複製代碼
若是記錄爲INFO
或更低,默認日誌處理器不會處理低於INFO
級別的信息,能夠經過修改日誌處理器的配置來改變這一情況。
默認的日誌記錄將顯示包含日誌調用的類名和方法名,如同堆棧所顯示的那樣。
可是若是虛擬機對執行過程進行了優化,就得不到準確的調用信息,此時,能夠調用logp
方法得到調用類和方法的確切位置,這個方法的簽名爲:
void logp(Level l, String className, String methodName, String message) 複製代碼
記錄日誌的常見用途是記錄那些不可預料的異常,可使用下面兩個方法提供日誌記錄中包含的異常描述內容:
if(...)
{
IOException exception = new IOException("...");
logger.throwing("com.mycompany.mylib.Reader", "read", exception);
throw exception;
}
複製代碼
還有
try
{
...
}
catch(IOException e)
{
Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
z
}
複製代碼
調用throwing
能夠記錄一條FINER
級別的記錄和一條以THROW
開始的信息。
剩餘部分暫時不作介紹,初步瞭解到這便可,一把要結合IDE一塊兒來使用這個功能。若是後續的高級知識部分有須要的話會單獨開設專題來介紹。
finally
子句try
語句我的靜態博客: