CORE JAVA 第七章 異常、斷言和日誌
第七章 異常、斷言和日誌
對於異常情況,例如,可能造成程式崩潰的錯誤輸入,Java使用一種稱為異常處理(exception handing)的錯誤捕獲機制處理。
在測試期間,需要進行大量的檢測以驗證程式操作的正確性。然而,這些檢測可能非常耗時,在測試完成後也不必保留它們,因此,可以將這些檢測刪掉,並在其他測試需要時將他們貼上回來,這是一件很乏味的事。斷言,可以有選擇地啟用檢測。
當程式出現錯誤時,並不總是能夠與使用者或終端進行溝通。此時,可能希望記錄下出現的問題,以備日後進行分析。本章的第三部分將討論標準Java日誌框架。
7.1 處理錯誤
假設在一個Java程式執行期間出現了一個錯誤。如果由於出現錯誤而使得某些操作沒有完成,程式應該:
- 返回到一種安全狀態,並能夠讓使用者執行一些其他的命令;或者
- 允許使用者儲存所有操作的結果,並以妥善的方式終止程式。
要做到這些並不是一件很容易的事。其原因是檢測(或引發)錯誤條件的程式碼通常離那些能夠讓資料恢復到安全狀態,或能夠儲存使用者的操作結果,並正常地退出程式的程式碼很遠。異常處理的任務就是將控制權從錯誤產生的地方轉移給能夠處理這種情況的錯誤處理器。
正如第五章中所敘述的那樣,在Java中,如果某個方法不能夠採用正常的途徑完整它的任務,就可以通過另外一個路徑退出方法。在這種情況下,方法並不返回任何值,而是丟擲一個封裝了錯誤資訊的物件。這個方法將會立刻退出,呼叫這個方法的程式碼也將無法繼續執行,取而代之的是異常處理機制開始搜尋能夠處理這種異常狀況的異常處理器。
7.1.1 異常分類
異常物件都是派生於Throwable類的一個例項。如果Java中內建的異常類不能夠滿足需求,使用者可以建立自己的異常類。
所有的異常都是由Throwable繼承而來,但在下一層分解為兩個分支:Error和Exception。
Error類層次結構描述了Java執行時系統的內部錯誤和資源耗盡錯誤。應用程式不應該丟擲這種型別的物件。如果出現了這樣的內部錯誤,除了通告給使用者,並盡力使程式安全地終止之外,再也無能為力了。這種情況很少出現。
Exception層次結構又分解為兩個分支:一個分支派生於RuntimeException;另一個分支包含其他異常。由於程式錯誤導致的異常屬於RuntimeException;而程式本身沒有問題,但由於像I/O錯誤這類問題導致的異常屬於其他異常。
派生於RuntimeException的異常包含下面幾種情況:
- 錯誤的型別轉換。
- 陣列訪問越界。
- 訪問null指標。
不是派生於RuntimeException的異常包含下面幾種情況:
- 試圖在檔案尾部後面讀取資料。
- 試圖開啟一個不存在的檔案。
- 試圖根據給定的字串查詢Class物件,而這個字串表示的類並不存在。
“如果出現RuntimeException異常,那麼一定就是你的問題”是一條相當有道理的規則。應該通過檢測陣列下標是否越界來避免ArrayIndexOutOfBoundsException異常;應該通過在使用變數之前檢測是否為null來杜絕NullPointerException異常的發生。
Java語言規範將派生於Error類或RuntimeException類的所有異常稱為非受查異常,所有其他的異常稱為受查異常。編譯器將核查是否為所有的受查異常提供了異常處理器。
7.1.2 宣告受查異常
如果遇到了無法處理的情況,那麼Java的方法可以丟擲一個異常。這個道理非常簡單:一個方法不僅要告訴編譯器將要返回什麼值,還要告訴編譯器有可能發生什麼錯誤。
方法應該在其首部宣告所有可能丟擲的異常。例如,下面是標準類庫中提供的FileInputStream類的一個構造器的宣告:
public FileInputStream(String name) throws FileNotFoundException
這個宣告表示這個構造器將根據給定的String引數產生一個FileInputStream物件,但也有可能丟擲一個FileNotFoundException異常。如果發生了這種糟糕情況,構造器將不會初始化一個新的FileInputStream物件,而是丟擲一個FileNotFoundException類物件,執行時系統就會開始搜尋異常處理器,以便知道如何處理FileNotFoundException物件。
在自己編寫方法時,不必將所有可能丟擲的異常都宣告。在下面4種情況時應該丟擲異常:
- 呼叫一個丟擲受查異常的方法,例如,FileInputStream構造器。
- 程式執行過程中發現錯誤,並且利用throw語句丟擲一個受查異常。
- 程式出現錯誤,例如,a[-1] = 0會丟擲一個ArrayIndexOutOfBoundsException這樣的非受查異常。
- Java虛擬機器和執行時庫出現的內部錯誤。
如果出現前兩種情況之一,則必須告訴呼叫這個方法的程式設計師有可能丟擲異常。如果沒有處理器捕獲這個異常,當前執行的執行緒就會結束。
對於那些可能被他人使用的方法,應該根據異常規範,在方法的首部宣告這個方法可能丟擲的異常。
class MyAnimation
{
……
public Image loadImage(String s) throws IOException
{
……
}
}
如果一個方法有可能丟擲多個受查異常型別,那麼就必須在方法的首部列出所有的異常類。每個異常類之間用逗號隔開。
class MyAnimation
{
……
public Image loadImage(String s) throws FileNotFoundException,EOFException
{
……
}
}
但是,不需要宣告Java的內部錯誤,即從Error繼承的錯誤。同樣,也不應該宣告從RuntimeException繼承的那些非受查異常。
class MyAnimation
{
……
void drawImage(int i) throws ArrayIndexOutOfBoundsException // bad style
{
……
}
}
總之,一個方法必須宣告所有可能丟擲的受查異常,而非受查異常要麼不可控制(Error),要麼就應該避免發生(RuntimeException)。如果方法沒有宣告所有可能發生的受查異常,編譯器就會發出一個錯誤訊息。
除了宣告異常之外,還可以捕獲異常。這樣會使異常不被拋到方法外,也不需要throws規範。稍後將會討論如何決定一個異常是被捕獲,還是被丟擲讓其他的處理器進行處理。
警告:如果在子類中覆蓋了超類的一個方法,子類方法中宣告的受查異常不能比超類方法中宣告的異常更通用(也就是說,子類方法中可以丟擲更特定的異常,或者根本不丟擲任何異常)。
如果超類方法沒有丟擲任何受查異常,子類也不能丟擲任何受查異常。
如果類中的一個方法宣告將會丟擲異常,而這個異常是某個特定類的例項時,則這個方法就有可能丟擲一個這個類的異常,或者這個類的任意一個子類的異常。
註釋:在Java中,沒有throws說明符的方法將不能丟擲任何受查異常。
7.1.3 如何丟擲異常
丟擲異常的語句:
throw new EOFException();
或者:
EOFException e = new EOFException();
throw e;
下面將這些程式碼放在一起:
String readData(Scanner in) throws EOFException
{
……
while(……)
{
if (!in.hasNext())
{
if (n <len)
throw new EOFException();
}
……
}
return s;
}
EOFException類還有一個含有一個字串型引數的構造器,這個構造器可以更加細緻的描述異常出現的情況。
String gripe = "Content-length: " + len +", Received " + n;
throw new EOFException(gripe);
對於一個已經存在的異常類,將其丟擲非常容易:
- 找到一個合適的異常類。
- 建立這個類的一個物件。
- 將物件丟擲。
一旦方法丟擲了異常,這個方法就不可能返回到呼叫者。也就是說,不必為返回的預設值或錯誤程式碼擔憂。
7.1.4 建立異常類
在程式中,可能會遇到任何標準異常類都沒有能夠充分地描述清楚的問題。建立自己的異常類需要做的是定義一個派生於Exception的類,或者派生於Exception子類的類。
習慣上,定義的類應該包含兩個構造器,一個是預設的構造器;另一個是帶有詳細描述資訊的構造器(超類Throwable的toString方法會打印出這些詳細資訊,這在除錯中非常有用)。
class FileFormatException extends IOException
{
public FileFormatException() {}
public FileFormatException(String gripe)
{
super(gripe);
}
}
現在,就可以丟擲自己定義的異常型別了。
7.2 捕獲異常
7.2.1 捕獲異常
如果某個異常發生的時候沒有在任何地方進行捕獲,那程式就會終止執行,並在控制檯上打印出異常資訊,其中包括異常的型別和堆疊的內容。
要想捕獲一個異常,必須設定try/catch語句塊。最簡單的try語句塊如下所示:
try
{
// codes
}
catch (ExceptionType e)
{
// handler for this type
}
如果在try語句塊中的任何程式碼丟擲了一個在catch子句中說明的異常類,那麼
- 程式將跳過try語句塊的其餘程式碼
- 程式將執行catch子句中的處理器程式碼
如果在try語句塊中的程式碼沒有丟擲任何異常,那麼程式將跳過catch子句。
如果方法中的任何程式碼丟擲了一個在catch子句中沒有宣告的異常型別,那麼這個方法就會立刻退出。
通常,應該捕獲那些知道如何處理的異常,而將那些不知道怎樣處理的異常繼續進行傳遞(傳遞給呼叫者)。
如果想傳遞一個異常,就必須在方法的首部新增一個throws說明符,以便告知呼叫者這個方法可能會丟擲異常。請記住,編譯器嚴格的執行throws說明符。如果呼叫了一個丟擲受查異常的方法,就必須對它進行處理,或者繼續傳遞。
仔細閱讀一下Java API文件,以便知道每個方法可能會丟擲哪種異常,然後再決定是自己處理,還是新增到throws列表中。對於後一種情況,也不必猶豫。將異常直接交給能夠勝任的處理器進行處理要比壓制對它的處理更好。
這個規則也有一個例外。如果編寫一個覆蓋超類的方法,而這個方法又沒有丟擲異常,則這個方法就必須捕獲方法程式碼中出現的每一個受查異常。
7.2.2 捕獲多個異常
在一個try語句塊中可以捕獲多個異常型別,並對不同型別的異常做出不同的處理。可以按照下列方式為每個異常型別使用一個單獨的catch子句:
try
{
// codes that might throw exceptions
}
catch (FileNotFoundException e)
{
……
}
catch (UnkownHostException e)
{
……
}
catch (IOException e)
{
……
}
e.getMessage()
得到詳細的錯誤資訊;e.getClass().getName()
得到異常物件的實際型別。
在Java SE 7中,同一個catch子句中可以捕獲多個異常型別。例如,假設對應缺少檔案和未知主機異常的動作是一樣的,就可以合併catch子句:
try
{
// codes that might throw exceptions
}
catch (FileNotFoundException | UnkownHostException e)
{
……
}
catch (IOException e)
{
……
}
只有當捕獲的異常型別彼此之間不存在子類關係時才需要這個特性。
註釋:捕獲多個異常時,異常變數隱含為final變數。不能為其賦不同的值。
註釋:捕獲多個異常,生成的位元組碼只包含一個對應公共catch子句的程式碼塊。
?
7.2.3 再次丟擲異常與異常鏈
在catch子句中可以丟擲一個異常,這樣做的目的是改變異常的型別。
下面給出了捕獲異常並將它再次丟擲的基本方法:
try
{
//
}
catch(SQLException e)
{
throw new ServletException("data error : " + e.getMessage());
}
不過,可以有一種更好的處理方法,並且將原始異常設定為新異常的“原因”:
try
{
//
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
當捕獲到異常的時候,就可以使用下面這條語句重新得到原始異常:
Throwable e = se.getCause();
強烈建議使用這種包裝技術。這樣可以讓使用者丟擲子系統中的高階異常,而不會丟失原始異常的細節。
提示:如果在一個方法中發生了一個受查異常,而不允許丟擲它,那麼包裝技術就十分有用。我們可以捕獲這個受查異常,並將它包裝成一個執行時異常。
有時你可能只想記錄一個異常,再將它重新丟擲,而不做任何改變:
try
{
//
}
catch (Exception e)
{
logger.log(level, message, e);
throw e;
}
7.2.4 finally子句
不管是否有異常被捕獲,finally子句中的程式碼都被執行。finally子句可以恰當地關閉一個檔案,或關閉與資料庫的連線。
在下面的示例中,程式將在所有情況下關閉檔案。
InputStream in = new FileInputStream(……);
try
{
//1
//code that might throw exceptions
//2
}
catch (IOException e)
{
//3
//show error message
//4
}
finally
{
//5
in.close();
}
// 6
三種情況:省略
try語句可以只有finally子句,而沒有catch子句。
事實上,我們認為在需要關閉資源時,用這種方式使用finally子句是一種不錯的選擇。
提示:強烈建議解耦合try/catch和try/finally語句塊,這樣可以提高程式碼清晰度。例如:
InputStream in = ……;
try
{
try
{
//code that might throw exceptions
}
finally
{
in.close();
}
}
catch (IOException e)
{
// show error message
}
內層的try語句塊只有一個職責,就是確保關閉輸入流。外層的try語句塊也只有一個職責,就是確保報告出現的錯誤。這種設計方式不僅清楚,而且還具有一個功能,就是將會報告finally子句中出現的錯誤。
有時候,finally子句也會帶來麻煩。例如,清理資源的方法也可能丟擲異常。假設在try語句塊中的程式碼丟擲了一些非IOException的異常,這些異常只有方法的呼叫者才能給予處理。執行finally語句塊,並呼叫close方法。而close方法本身也有可能丟擲IOException異常。當出現這種情況時,原始的異常將會丟失,轉而丟擲close方法的異常。
如果想做適當的處理,重新丟擲原來的異常,程式碼會變得極其繁瑣。如下:
InputStream in = ……;
Exception ex = null;
try
{
try
{
//code that might throw exceptions
}
catch (Exception e)
{
ex = e;
throw e;
}
}
finally
{
try
{
in.close();
}
catch (Exception e)
{
if (ex == null) throw e;
}
}
下一節將瞭解到,Java SE 7中關閉資源的處理會容易很多。
7.2.5 帶資源的try語句
對於以下程式碼模式:
open a resource
try
{
work with the resource
}
finally
{
close the resource
}
假設資源屬於一個實現了AutoCloseable介面的類,Java SE 7為這種程式碼模式提供了一個很有用的快捷方式。AutoCloseable介面有一個方法:
void close() throws Exception
註釋:另外,還有一個Closeable介面。這是AutoCloseable的子介面,也包含一個close方法,不過,這個方法宣告為丟擲一個IOException。
帶資源的try語句(try-with-resources)的最簡形式為:
try (Resource res = ……)
{
work with res
}
try塊退出(正常退出或存在異常)時,會自動呼叫res.close()
。就好像使用了finally塊一樣。
上一節已經看到,如果try塊丟擲一個異常,而且close方法也丟擲一個異常,這就會帶來一個難題。帶資源的try語句可以很好地處理這種情況。原來的異常會重新丟擲,而close方法丟擲的異常會被“抑制”。這些異常將自動捕獲,並由addSuppressed方法增加到原來的異常。如果對這些異常感興趣,可以呼叫getSuppressed方法,它會得到從close方法丟擲並被抑制的異常列表。
只要需要關閉資源,就要儘可能使用帶資源的try語句。
註釋:帶資源的try語句自身也可以有catch子句和一個finally子句。這些子句將在關閉資源之後執行。
7.2.6 分析堆疊軌跡元素
堆疊軌跡(stack trace)是一個方法呼叫過程的列表,它包含了程式執行過程中方法呼叫的特定位置。當Java程式正常終止,而沒有捕獲異常時,這個列表就會顯示出來。
可以呼叫Throwable類的printStackTrace方法訪問堆疊軌跡的文字描述資訊。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(mew PrintWriter(out));
String description = out.toString();
一種更靈活的方式是使用getStackTrace方法,它會得到StackTraceElement物件的一個數組,可以在你的程式中分析這個物件陣列。例如:
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
analyze frame
StackTraceElement類含有能夠獲得檔名和當前執行的程式碼行號的方法,同時,還含有能夠獲得類名和方法名的方法。toString方法將產生一個格式化的字串,其中包含所獲得的資訊。
靜態的Thread.getAllStackTrace方法,它可以產生所有執行緒的堆疊軌跡。下面給出使用這個方法的具體方式:
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet())
{
StackTraceElement[] frames = map.get(t);
analyze frames
}
7.3 使用異常機制的技巧
-
異常處理不能代替簡單的測試
試著上百萬次地對一個空棧進行退棧操作。在實施退棧之前,首先要檢視棧是否為空:
if (!s.empty()) s.pop();
接下來,強行進行退棧操作。然後,捕獲EmptyStackException異常來告知我們不能這樣做。
try
{
s.pop();
}
catch (EmptyStackException e)
{
}
在測試的機器上,呼叫isEmpty的版本執行時間為646毫秒。捕獲EmptyStackException的版本執行時間為為21739毫秒。
可以看出,與執行簡單的測試相比,捕獲異常所花費的時間大大超過了前者,因此使用異常的基本規則是:只在異常情況下使用異常機制。
- 不要過分地細化異常
不要將每一條語句都分裝在一個獨立的try語句塊中。有必要將整個任務包裝在一個try語句塊中,這樣,當任何一個操作出現問題的時候,整個任務都可以取消。這樣也滿足了異常處理機制的其中一個目標,將正常處理與錯誤處理分開。
- 利用異常層次結構
不要只丟擲RunTimeException異常。應該尋找更加適當的子類或建立自己的異常類。
不要只捕獲Throwable異常。否則,會使程式程式碼更難讀、更難維護。
已檢查異常本來就很龐大,不要為邏輯錯誤丟擲這些異常。
將一種異常轉換成另一種更加適合的異常時不要猶豫。
- 不要壓制異常
在Java中,往往強烈地傾向關閉異常。
- 在檢測錯誤時,“苛刻”比放任更好
在用無效的引數呼叫一個方法時,返回一個虛擬的數值,還是丟擲一個異常?例如,當棧空時,stack.pop是返回一個null,還是丟擲一個異常?我們認為:在出錯的地方丟擲一個EmptyStackException異常要比在後面丟擲一個NullPointerException異常更好。
- 不要羞於傳遞異常
如果呼叫了一個丟擲異常的方法,這些方法就會本能地捕獲這些可能產生的異常。其實,傳遞異常比捕獲這些異常更好。
讓高層次的方法通知使用者發生了錯誤,或者放棄不成功的命令更加適宜。
規則5、6可以歸納為“早丟擲,晚捕獲”。
7.4 使用斷言
在一個具有自我保護能力的程式中,斷言很常用。
7.4.1 斷言的概念
假設確信某個屬性符合要求,並且程式碼的執行依賴於這個屬性。例如,需要計算double y = Math.sqrt(x)
。
我們確信,這裡的x是一個非負數值。原因是:x是另外一個計算的結果,而這個結果不可能是負值;或者x是一個方法的引數,而這個方法要求它的呼叫者只能提供一個正整數。
然而,還是希望進行檢查,以避免讓“不是一個數”的數值參與計算操作。當然,也可以丟擲一個異常:
if (x < 0) throw new IllegalArgumentException("x < 0");
但是這段程式碼會一直保留在程式中,即使測試完畢也不會自動地刪除。如果在程式中含有大量的這種檢查,程式執行起來會相當慢。
斷言機制允許在測試期間向程式碼中插入一些檢測語句。當代碼釋出時,這些插入的檢測語句將會被自動地移走。
Java語言引入了關鍵字assert。這個關鍵字有兩種形式:
assert 條件;
assert 條件:表示式;
這兩種形式都會對條件進行檢測,如果結果為false,則丟擲一個AssertionError異常。在第二種形式中,表示式將被傳入AssertionError的構造器,並轉換成一個訊息字串。
表示式部分的唯一目的是產生一個訊息字串。AssertionError物件並不儲存表示式的值,因此,不可能在以後得到它。
要想斷言x是一個非負數值,只需要簡單地使用下面這條語句:
assert x >= 0;
或者將x的實際值傳遞給AssertionError物件,從而可以在後面顯示出來:
assert x>= 0 : x;
個人理解:
用易理解的話來說 ,斷言主要使用在程式碼開發和測試時期,用於對某些關鍵資料的判斷,如果這個關鍵資料不是你程式所預期的資料,程式就提出警告或退出。
7.4.2 啟用和禁用斷言
在預設情況下, 斷言被禁用。可以在執行程式時使用-enableassertions
或-ea
選項啟用:
java -enableassretions MyApp
需要注意的是,在啟用或禁用斷言時不必重新編譯程式。啟用或禁用斷言是類載入器的功能。當斷言被禁用時,類載入器將跳過斷言程式碼,因此,不會降低程式執行的速度。
也可以在某個類或整個包中使用斷言,例如:
java -ea:MyClass -ea:com.mycompany.mylib……MyApp
選項-ea
將開啟預設包中的所有類的斷言。
選項-disableassertions
或-da
禁用某個特定類和包的斷言:
java -ea:…… -da:MyClass MyApp
然而,啟用和禁用所有斷言的-ea
和-da
開關不能應用到那些沒有類載入器的“系統類”上。對於這些系統類,需要使用-enablesystemassertions/-esa
開關啟用斷言。
7.4.3 使用斷言完成引數檢查
- 斷言失敗是致命的、不可恢復的錯誤。
- 斷言檢查只用於開發和測試階段。
因此,不應該使用斷言向程式的其他部分通告發生了可恢復性的錯誤,或者,不應該作為程式向用戶通告問題的手段。斷言只應用於在測試階段確定程式內部的錯誤位置。
例子:檢查方法的引數。如果在開頭的文件中規定引數引用不能為null,就可以在方法的開頭使用斷言。
這種約定稱為前置條件。如果呼叫者在呼叫這個方法時沒有提供滿足這個前置條件的引數,所有的斷言都會失敗,並且這個方法可以執行它想做的任何操作。事實上,由於可以使用斷言,當方法被非法呼叫時,將會出現難以預料的結果。有時候會丟擲一個斷言錯誤,有時候會產生一個null指標異常,這完全取決於類載入器的配置。
7.4.4 為文件假設使用斷言
很多程式設計師使用註釋說明假設條件:
if (i % 3 == 0)
……
else if (i % 3 == 1)
……
else // (i % 3 == 2)
……
在這個示例中,使用斷言會更好一些。
if (i % 3 == 0)
……
else if (i % 3 == 1)
……
else
{
assert i % 3 == 2;
}
這個示例說明了程式設計師如何運用斷言來進行自我檢查。
!7.5 記錄日誌
斷言是一種測試和除錯階段所使用的戰術性工具;而日誌記錄是一種在程式的整個生命週期都可以使用的策略工具。
記錄日誌API;記錄日誌API的優點;
7.5.1 基本日誌
要生成簡單的日誌記錄,可以使用全域性日誌記錄器(global logger)並呼叫其info方法:
Logger.getGlobal().info("File -> Open menu item selected");
如果在適當的地方(如main開始)呼叫
Logger.getGlobal().setLevel(Level.OFF);
將會取消所有的日誌。
7.5.2 高階日誌
從前面已經看到“虛擬日誌”,下面繼續看一下企業級日誌。在一個專業的應用程式中,不要將所有的日誌都記錄到一個全域性日誌記錄器中,而是可以自定義日誌記錄器。
可以呼叫getLogger方法建立或獲取記錄器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
提示:未被任何變數引用的日誌記錄器可能會被垃圾回收。為了防止這種情況發生,要用一個靜態變數儲存日誌記錄器的一個引用。
與包名類似,日誌記錄器名也具有層次結構。日誌記錄器的父與子之間將共享某些屬性。例如,如果對com.mycompany日誌記錄器設定了日誌級別,它的子記錄器也會繼承這個級別。
通常,有以下7個日誌記錄器級別:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
在預設情況下,只記錄前三個級別。也可以設定其他的級別。例如:
logger.setLevel(Level.FINE);
現在,FINE和更高級別的記錄都可以記錄下來。
另外,還可以使用Level.ALL開啟所有級別的記錄,或者使用Level.OFF關閉所有級別的記錄。
對於所有的級別有下面幾種記錄方法:
logger.warning(message);
logger.fine(message);
同時,還可以使用log方法指定級別:
logger.log(Level.FINE, message);
預設的日誌記錄將顯示包含日誌呼叫的類名和方法名,如同堆疊所顯示的那樣。但是,如果虛擬機器對執行過程進行了優化,就得不到準確的呼叫資訊。此時,可以呼叫logp方法獲得呼叫類和方法的確切位置,這個方法的簽名為:
void logp(Level l, String className, String methodName, String message)
entering和exiting方法可以跟蹤執行流。這些呼叫將生成FINER級別和以字串ENTRY和RETURN開始的日誌記錄。
記錄日誌的常見用途是記錄那些不可預料的異常。可以使用throwing和log方法提供日誌記錄中包含的異常描述內容。
呼叫throwing可以記錄一條FINER級別的記錄和一條以THROW開始的資訊。
7.5.3 修改日誌管理器配置
可以通過編輯配置檔案來修改日誌系統的各種屬性。比如,修改預設的日誌記錄級別。
警告:日誌管理器在VM啟動過程中初始化,這在main執行之前完成。
日誌記錄並不將訊息傳送到控制檯上,這是處理器的任務。
7.5.4 本地化
將日誌訊息本地化,以便全球的使用者都可以閱讀它。
7.5.5 處理器
在預設情況下,日誌處理器將記錄傳送到ConsoleHandler中,並由它輸出到System.err流中。
………………
7.5.6 過濾器
7.5.7 格式化器
7.5.8 日誌記錄說明
- 為一個簡單的應用程式,選擇一個日誌記錄器,並把日誌記錄器命名為與主應用程式包一樣的名字,是一種好的程式設計習慣。可以通過呼叫下列方法得到日誌記錄器。
Logger logger = Logger.getLogger("com.mycompany.myprog");
- 預設的日誌配置將級別等於或高於INFO級別的所有訊息記錄到控制檯。
- 所有級別為INFO、WANING和SEVERE的訊息都將顯示在控制檯上。因此,最好只將對程式使用者有意義的訊息設定為這幾個級別。將程式設計師想要的日誌記錄,設定為FINE是一個很好的選擇。
7.6 除錯技巧
假設編寫了一個程式,並對所有的異常進行了捕獲和恰當的處理,然後,執行這個程式,但還是出現問題,現在該怎麼辦呢?
在啟動偵錯程式前,本節先給出一些有價值的建議。
- 列印或記錄任意變數的值。
- 在每一個類中放置一個單獨的main方法。這樣就可以對每一個類進行單元測試。利用這種技巧,只需要建立少量的物件,呼叫所有的方法,並檢測每個方法是否能夠正確地執行就可以了。
- 使用Junit。Junit是一個非常常見的單元測試框架。
- 日誌代理可以擷取方法呼叫,並進行日誌記錄,然後呼叫超類中的方法。
- 利用Throwable類提供的printStackTrace方法。可以從任何一個異常物件中獲得堆疊情況。
……