《Java核心技術 卷Ⅰ》 第7章 異常、斷言和日誌
- 處理錯誤
- 捕獲異常
- 使用異常機制的技巧
- 記錄日誌
處理錯誤
如果由於出現錯誤而是的某些操作沒有完成,程式應該:
- 返回到一種安全狀態,並讓使用者執行一些其他操作;或者
- 允許使用者儲存所有操作,並以妥善方式終止程式
檢測(或引發)錯誤條件的程式碼通常離:
- 能讓資料恢復到安全狀態
- 能儲存使用者的操作結果並正常退出程式
的程式碼很遠。
異常處理的任務:將控制權從錯誤產生地方轉移給能夠處理這種情況的錯誤處理器。
異常分類
在Java中,異常物件都是派生於Throwable
類的一個例項,如果Java中內建的異常類不能滿足需求,使用者還可以建立自己的異常類。
Java異常層次結構:
-
Throwable
-
Error
- …
-
Exception
-
IOException
- …
-
RuntimeException
- …
-
-
可以看到第二層只有Error
和Exception
。
Error
類層次結構描述了Java執行時系統的內部錯誤和資源耗盡錯誤,應用程式不應該丟擲這種型別的物件,這種內部錯誤的情況很少出現,出現了能做的工作也很少。
設計Java程式時,需關注Exception
層次結構,這個層次又分為兩個分支,RuntimeException
和包含其他異常的IOException
。
劃分兩個分支的規則是:
- 由程式錯誤導致的異常屬於
RuntimeException
- 程式本身無問題,由於像I/O錯誤這類問題導致的異常屬於其他異常
IOException
派生於RuntimeException
的異常包含下面幾種情況:
- 錯誤型別轉換
- 陣列訪問越界
- 訪問null指標
派生於IOException
的異常包含下面幾種情況:
- 試圖在檔案尾部後面讀取資料
- 試圖開啟一個不存在的檔案
- 試圖根據指定字串查詢
Class
物件,而這個字串表示的類並不存在
Java語言規範將派生於Exception
類和RuntimeException
類的所有異常統稱非受查(unchecked)異常,所有其他異常都是受查(checked)異常。
編譯器將核查是否為所有的受查異常提供了異常處理器。
宣告受查異常
一個方法不僅要告訴編譯器將要返回什麼值,還要告訴編譯器有可能發生什麼錯誤。
異常規範(exception specification):方法應該在其首部宣告所可能丟擲的異常。
public FileInputStream(String name) throws FileNotFoundException
如果這個方法丟擲了這樣的異常物件,執行時系統會開始搜尋異常處理器,以便知道如何處理這個異常物件。
當然不是所有方法都需要宣告異常,下面4種情況應該丟擲異常:
- 呼叫一個丟擲受查異常的方法時
- 程式執行過程中發現錯誤,並且利用
throw
語句丟擲一個受查異常 - 程式出現錯誤,一般是非受查異常
- Java虛擬機器和執行時庫出現的內部錯誤
出現前兩種情況之一,就必須告訴呼叫者這個方法可能的異常,因為如果沒有處理器捕獲這個異常,當前執行的執行緒就會結束。
如果一個方法有多個受查異常型別,就必須在首部列出所有的異常類,異常類之間用逗號隔開:
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();
這樣可以讓使用者丟擲子系統中的高階異常,而不會丟失原始異常的細節。
finally子句
當程式碼丟擲一個異常時,就會終止方法中剩餘程式碼的處理,並退出這個方法的執行。
如果方法獲得了一些本地資源,並且只有這個方法自己知道,又如果這些資源在退出方法之前必須被回收(比如資料庫連線的關閉),那麼就會產生資源回收問題。
一種是捕獲並重新丟擲所有異常,這種需要在兩個地方清除所分配的資源,一個在正常程式碼中,另一個在異常程式碼中。
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
子句:
- 程式碼沒有丟擲異常,執行序列為1、2、5、6
-
丟擲一個在
catch
子句中捕獲的異常- 如果
catch
子句沒有丟擲異常,執行序列為1、3、4、5、6 - 如果
catch
子句丟擲一個異常,異常將被拋回這個方法的呼叫者,執行序列為1、3、5(注意沒有6)
- 如果
- 程式碼丟擲了一個異常,但是這個異常沒有被捕獲,執行序列為1、5
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中關閉資源的處理會容易很多。
帶資源的try語句
對於以下程式碼模式:
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
}
java.lang.Throwable
Throwable(Throwable cause)
Throwable(String message, Throwable cause)
-
Throwable initCause(Throwable cause)
:將這個物件設定為“原因”,如果這個物件已經被設定為“原因”,則丟擲一個異常,返回this
引用。 -
Throwable getCause()
:獲得設定為這個物件的“原因”的異常物件,如果沒有則為null
-
StackTraceElement[] getStackTrace()
:獲得構造這個物件時呼叫堆疊的跟蹤 -
void addSuppressed(Throwable t)
:為這個異常增加一個抑制異常 -
Throwable[] getSuppressed()
:得到這個異常的所有抑制異常
java.lang.StackTraceElement
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一起來使用這個功能。如果後續的高階知識部分有需要的話會單獨開設專題來介紹。
Java異常、斷言和日誌總結
- 處理錯誤
- 異常分類
- 受查異常
- 丟擲異常
- 建立異常類
- 捕獲異常
- 再次丟擲異常與異常鏈
-
finally
子句 - 在資源的
try
語句 - 分析堆疊軌跡元素
- 使用異常機制的技巧
- 基本日誌與高階日誌
個人靜態部落格:
- 氣泡的前端日記: https://rheabubbles.github.io