11 異常、斷言、日志和調試
異常處理(exception handing)
使用斷言來啟動檢測
Java日志框架
調試技巧
11.1 處理錯誤
如果一個方法不能夠采用正常的途徑完成任務,就通過另外一個路徑退出方法。
在這種情況下,方法不返回任何值,而是拋出一個封裝了錯誤信息的對象。此外,調用這個方法的代碼也將無法繼續執行。
異常處理機制開始搜索能夠處理這種異常情況的異常處理器(exception handler)。
異常處理的任務就是將控制權從錯誤產生的地方轉移給能夠處理這種情況的錯誤處理器。
?
異常對象是派生于Throwable類的一個實例。
可以創建自己的異常類。
?
異常的分類:
Throwable | Exception | RuntimeException | ? |
ArrayIndexOutOfBoundsException | |||
NullPointerException | |||
IOException | ? | ||
? | |||
Error | ? | ? |
?
Error類層次描述的是Java運行時系統的內部錯誤和資源耗盡。
應用程序不應拋出此類錯誤。
除了通告給用戶并盡力使程序安全地中止之外,再也無能為力。
此種情況很少出現。
?
派生于Error類或RuntimeException類的所有異常稱為未檢查(unchecked)異常;
所有其他的異常稱為已檢查(checked)異常。
編譯器將檢查是否為所有已檢查異常提供了異常處理器。
?
聲明已檢查異常:
public FileInputStream(String name) throws?FileNotFoundException
?
一個方法要么返回其返回值,要么拋出一個異常。
一個方法必須聲明所有可能拋出的已檢查異常,而未檢查異常要么不可控(Error),要么應避免(RuntimeException)。
如果子類中覆蓋了超類的一個方法,子類方法中聲明的已檢查異常不能比超類方法中聲明的異常更通用。
?
如何拋出異常:
1、找到一個合適的異常類;
2、創建其類對象;
3、將對象拋出。
如:throw new EOFException();
throw new EOFException(String);
?
一旦方法拋出異常,這個方法就不能返回到調用者。
C++中可以拋出任何類型的值,Java中只能拋出Throwable子類的對象。
?
創建異常類:
定義一個派生于Exception的類,或者派生于Exception子類的類。
包含兩個構造器:默認構造器;帶有詳細描述信息的構造器。
class FileFormatException extends IOException
{public FileFormatException(){}public FileFormatException(String message){super(massage);}
}
11.2 捕獲異常
如果某個異常發生的時候沒有在任何地方進行捕獲,那程序就會終止執行,并在控制臺上打印出異常信息,其中包括異常的類型和堆棧信息。
捕獲異常:
try
{code that might throw exception
}
catch(FileNotFoundException | UnknownHostException e) //捕獲多個異常時,e隱含為final變量。
{emergency action for missing files and unknown hosts
}
catch(IOException e)
{emergency action for all other I/O problems
}
如果try語句塊中的任何代碼拋出了一個在catch子句中說明的異常類,則跳過try語句塊的其余代碼,執行catch子句中的處理器代碼;
如果try語句塊中拋出的異常沒有在catch子句中說明,則立即退出這個方法;
如果try語句塊沒有拋出異常,則跳過catch子句。
?
通常,異常處理的最好選擇是什么都不做,而是將異常傳遞給調用者。
如果調用了一個拋出已檢查異常的方法,要么處理,要么傳遞。
通常,捕獲那些知道如何處理的異常,傳遞那些不知怎樣處理的異常。
?
在catch子句中還可以拋出一個異常,這樣做目的是改變異常的類型。
如:
try
{access the database
}
catch (SQLException e)
{Throwable se = new ServletException(“database error”);se.initCause(e);throw se;
}
finally
{close resource;
}Throwable e = se.getCause();
//可以得到原始異常,使用這種包裝技術,可以讓用戶拋出子系統中的高級異常,而不會丟失原始異常的細節。
如果在一個方法中發生了一個已檢查異常,而方法不允許拋出它,那么包裝技術可以將這個已檢查異常包裝成一個運行時異常。
?
強烈建議使用try/catch、try/finally子句。
finally子句一定會被執行。
?
帶資源的try語句
try(Resource res = ...)
{work with res;
}
//try塊退出時,自動調用res.close()方法。
//此時,try塊拋出的異常被拋出,close方法拋出的異常被抑制。
帶資源的try語句自身也可以有catch子句和一個finally子句。這些子句會在關閉資源后執行。
在實際中,一個try語句中加入這么多內容可能不是一個好主意。
?
堆棧跟蹤(stack trace):
是一個方法調用過程的列表,包含了程序執行過程中方法調用的特定位置。
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)analyze frame;
//多線程堆棧跟蹤
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet())
{StachTraceElement[] frames = map.get(t);analyze frames
}
11.3 使用異常的技巧
1、異常處理不能代替簡單的測試;
2、不要過分細化異常;
3、利用異常層次結構;
4、不要壓制異常;對于不常發生的異常,catch語句塊的內容可以先空著,待日后感覺需要處理時再填上。
5、不要羞于傳遞異常。
?
11.4 使用斷言
斷言機制允許在測試期間向代碼中插入一些檢查語句。
當代碼發布時,這些插入的檢測語句將會被自動移走。
?
assert 條件;
assert 條件:表達式;
?
這兩種形式都會對條件進行檢測,如果結果為false,則拋出一個AssertionError異常。
在第二種形式中,表達式將被傳入AssertionError的構造器,并轉換成一個消息字符串。
表達式的唯一目的就是產生一個消息字符串。
?
assert x >= 0;
assert x >=0 : x;
assert x >=0 “x >= 0”;
?
默認情況下,斷言被禁用。
在運行時,用-enableassertions 或 -ea 選項啟用它。
java -enableassertions MyApp
在啟用或禁用斷言時不必重新編譯程序。啟用或禁用斷言時類加載器(class loader)的功能。
當斷言被禁用時,類加載器將跳過斷言代碼,因此不會降低程序運行的速度。
也可選用-disableassertions或-da禁用某個特定類或包的斷言。
?
在Java中,給出了三種處理系統錯誤的機制:
異常;
日志;
斷言。
?
斷言是致命的,不可恢復的錯誤。只用于開發和測試階段。
斷言是一種測試和調試階段所使用的戰術性工具;
日志記錄是一種在程序的整個生命都可以使用的策略性工具。
11.5 記錄日志
優點:
·可以取消全部日志記錄,或僅取消某個級別的日志,而且打開和關閉這個操作也很容易;
·可以很簡單的禁止日志記錄的輸出,因此,這些日志代碼留在程序中的開銷很小;
·日志記錄可以在控制臺中顯示,也可以在文件中存儲;
·可以對日志記錄進行過濾,只保留重要日志;
·可以采用不同格式:純文本或XML;
·可使用多個日志記錄;
·默認情況下,日志系統的配置由配置文件控制;如果需要的話,應用程序可以替換這個配置。
?
基本日志:
日志系統管理著一個名為Logger.global的默認日志記錄器,可通過info方法記錄日志信息
Logger.getGlobal().info(“File->Open menu item selected”); //記錄日志
Logger.getGlobal().setLevel(Level.OFF); //取消所有日志
?
高級日志:
在一個應用程序中,不要將所有的日志都記錄到一個全局日志記錄器中,而是自定義日志記錄器。
調用getLogger方法可以創建或檢索記錄器:
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”);
日志記錄具有層次結構,上下層之間將共享某些屬性,例如對com.mycompany日志記錄器設置了日志級別,它的子記錄器也會繼承這個級別。
SERVER ?WARNING ?INFO ?CONFIG ?FINE ?FINER ?FINEST
默認記錄前三個級別。
?
級別設置:
logger.setLevel(Level.FINE); //Level.ALL開啟所有級別 Level.OFF關閉所有級別
?
記錄方法:
logger.waring(message);
logger.fine(message);
logger.log(level.FINE, message);
?
默認的日志配置記錄了INFO或更高級別的所有記錄。
應該使用CONFIG、FINE、FINEST級別來記錄那些有助于診斷,但對于程序員又沒有太大意義的調試信息。
?
默認的日志記錄將顯示包含日志調用的類名和方法名,如同堆棧所顯示的那樣。
但是,如果虛擬機對執行過程進行了優化,就得不到準確的調用信息。
此時可用logp方法獲得調用類和方法的確切位置:
void logp(Level l, String className, String methodName, String Message)
?
?
跟蹤執行流的方法:
void entering(String className, String methodName)
void entering(String className, String methodName, Object param)
void entering(String className, String methodName, Object[] params)
void exiting(String className, String methodName)
void exiting(String className, String methodName, Object result)
如:
int read(String file, String pattern)
{logger.entering(“com.mycompany.mylib.Reader”,”read”,new Object[]{file, pattern};...logger.exiting(“com.mycompany.mylib.Reader”,”read”,count);return count;
}
這些調用將產生FINER級別和以字符串ENTRY和RETURN開始的日志記錄。
?
日志記錄的常見用途是記錄那些不可預料的異常。
兩種方法:
void throwing(String className, String methodName, Throwable t)
void log(Level l, String message, Throwable t)
典型用法:
if(...)
{IOException exception = new IOException(“...”);logger.throwing(“com.mucompany.mylib.Reader”,”read”,exception);throw exception;
}
//調用throwing可以記錄一條FINER級別的記錄和一條以THROW開始的信息。try
{...
}
catch(IOException)
{Logger.getLogger(“com.mycompany.myapp”).log(Level.WARNING, “Reading image”, e);
}
修改日志管理器配置文件jre/lib/logging.properties
日志管理器在VM啟動過程中被初始化,在main之前執行。
.level=INFO //修改默認的日志記錄級別
com.mycompany.myapp.level=FINE 指定自己的日志記錄級別
?
本地化:
本地化的應用程序包含資源包(resource bundle)中的本地特定信息。
資源包由各個地區的映射集合組成。如某個資源包可以將redingFile映射成英文的Reading file或者德文的Achtung! Datei wired eingelesen。
一個程序可以包含多個資源包,一個用于菜單;其他用于日志消息。
每個資源包都有一個名字,如com.mycompany.logmessages。
要想將映射添加到一個資源包中,需要為每個地區創建一個文件。
英文消息映射位于com/mycompany/logmessages_en.properties文件中;
德文消息映射位于com/mycompany/logmessages_de.properties文件中。
可以將這些文件與應用程序的類文件放在一起,以便ResourceBundle類自動對它們進行定位。
這些文件都是純文本文件,內容如下:
readigFile=Achtung! Datei wird eingelesen
renamingFile=Datei wird umbenannt
?
在請求日志記錄器時,可以指定一個資源包:
Logger logger= Logger.getLogger(loggerName, “com.mycompany.logmessages”);
然后,為日志消息指定資源包的關鍵字,而不是實際的日志消息字符串:
logger.info(“readingFile”);
?
通常需要在本地化的消息中增加一些參數,因此,消息應該包括占位符{0}、{1}等。
例如,在日志消息中包含文件名,需要使用下列方式包含占位符:
readingFile=Reading file {0}
renamingFile=Change name file {0} to {1}
?
logger.log(Level.INFO, “readingFile”, fileName);
logger.log(Level.INFO, “renamingFile”, new Object[]{oldName, newName};
?
處理器:
默認情況下,日志記錄器將記錄發送到ConsoleHandler中,并由它輸出到System.err流中。
日志記錄器還會將記錄發送到父處理中,最終處理器(命名為””)有一個ConsoleHandler。
處理器也有一個日志記錄級別。
?
安裝自己的處理器:
Logger logger = Logger.getLogger(“com.mycompany.myapp”);
logger.setLevel(Level.FINE);
logger.setUseParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler);
?
其他處理器:
FileHandler 將記錄發送到用戶主目錄的javan.log文件中,n是文件的唯一編號,默認XML格式。
SocketHandler 將記錄發到特定的主機和端口。
?
?
過濾器:
默認情況下,過濾器根據日志記錄的級別進行過濾。
每個日志記錄器和處理器都可以有一個可選的過濾器來完成附加的過濾。
另外,可通過實現Filter接口的boolean isLoggable(LogRecord record)方法來自定義過濾器。
調用setFilter方法安裝過濾器。
一個時刻最多只能有一個過濾器。
?
日志記錄說明:日志記錄器最好與主應用程序包名相同;