1 异常层次
需要考察的异常情况有:用户输入错误、设备错误、物理限制、程序错误,传统的返回错误码的方法并不能处理所有的情况。
1.1 异常分类
在Java程序中,异常对象都派生于Throwable。如果Java的内置异常不满足需求,则可以创建自定义异常。下图是基本的异常体系:
Error类描述了Java运行时系统的内部错误和资源耗尽错误,此类错误不应由应用程序抛出。
而Exception分两类,一类是因程序逻辑问题,导致系统出问题,此时就是Runtime异常,如数组越界、类型转换错误、访问空指针。而如果程序可以正常,但在遇到不同IO情况时,出问题导致,则属于IO异常,如打开格式错误的URL、在文件尾部读数据、加载不存在的类文件等。
RuntimeException一定是程序写的有问题,是可以避免出现的。Java将Error和RuntimeException定义为未检查的异常。其他异常为已检查。编译器将检查代码是否为所有已检查异常提供异常处理器。
1.2 异常声明
方法应该在首部声明其所有可能抛出的异常,例如:
public void lookup() throws XXException,YYException
一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控,要么就应该避免发生。如果方法没有声明所有可能发生的已检查异常,编译器就会给出错误消息。
当程序调用一个抛出已检查异常的方法、或程序自己会抛出异常时,有必要进行异常声明,对于Error和Runtime这些未检查异常,无人可以预料,因此不需要声明。
1.3 自定义异常
如果遇到标准异常不能说明的情况,则需要自定义异常。只需要继承自某个异常类,并定义两个构造函数即可。一个默认构造函数,一个带有描述信息的构造器。
2 捕获异常
异常的捕获需要周密的计划。如果异常没有被捕获,程序会终止,并会在控制台上输出异常信息。对UI程序,异常出现后会提示用户,用户可以继续返回到程序中。
使用try块来捕获异常
try
{
}
catch(XXXException e)
{
}
catch(YYYException e)
{
}
如果try语句中的任何代码抛出一个在catch中指定的异常,则程序会跳过try语句中的其余代码转而执行catch子句中的代码。如果抛出的是不在catch中指定的异常,则程序立即返回到上层调用者。
当捕获到异常后,一种方法是我们自己处理;另一种是不处理,交给调用者去处理,这样只需要声明异常即可。如果应该捕获那些知道如何处理的异常,而将不知道如何处理的异常传递给上层。将异常交给足以胜任的处理器,比压抑它好得多。
2.1 捕获多个异常
通过示例代码,可以捕获多个异常
2.2 链式异常
当捕获到异常后,我们可以改变其类型,并重新抛出。这样做可以隐藏底层异常的细节,保持异常解释的抽象性。为了获得真正的底层异常,可以用setCause()方法将底层异常包装到高层异常中,并在需要的时候用getCause()来获取。
2.3 finally子句
不论异常是否发生,finally中的语句都会被执行,这可以作为一种比较合适的清理资源的方式。当然如果在finally中抛出异常,则会导致try中异常类型丢失。因此建议异常的使用风格如下:
try
{
try
{
}
finally
{
}
}
catch()
{
}
内层的try..finally负责清理资源,而外层的try只负责报告错误。
2.4 堆栈跟踪
java.lang.Throwable
Throwable(String s)
Throwable(Throwable cause)
Throwable(String s,Throwable cause) 用给定的cause构造一个异常
Throwable getCause()
void setCause()
String getMessage() 获得描述信息
StackTraceElement[] getStackTrace() 获取构造这个对象时调用堆栈的跟踪
java.lang.Exception
Exception(Throwable cause)
Exception(String s,Throwable cause) 用给定的cause构造一个异常
java.lang.RuntimeException
RuntimeException(Throwable cause)
RuntimeException(String s,Throwable cause) 用给定的cause构造一个异常
java.lang.StackTraceElement
String getFileName() 返回元素运行时对应的源文件名
int getLineNumber() 返回元素运行时对应的源文件行数
String getClassName() 返回元素运行时类命名
String getMethodName() 返回方法名
bool isnativeMethod()
2.5 异常机制使用建议
I 异常机制对于性能损耗较大,最好使用简单测试来避免异常发生,如在stack非空时才调用pop,比使用try包围的pop性能高100倍
II 不要过分细分异常,将一段正常的流程放在一个try块中而不是分到不同的try块中,有得于代码的阅读。
III 善于利用异常层次,不要只抛出RuntimeException,而应该寻找和创建更适合的子类;也不要只捕获Throwable,否则代码会较难阅读。
IV 合理压制异常。如果认为异常不是问题则可以合理的压制,否则则应该声明或抛出。
V 早抛出,晚捕获;在程序出错的地方立即抛出异常,而尽可能向高层传递非细节异常。
3 记录日志
3.1 logging包的类结构
3.2 全局日志
日志模块,拥有一个全局变量Logger.global,可以用它向控制台输出信息。
Logger.global.info(String s) 打印日志到标准输出
Logger.global.setLevel(Level level) 设置日志级别
3.3 命名日志
日志也具有层次结构,不同的包可以按结构名来获取各自的日志,把记录器命名与主程序包一样的名字是一个好的实践。
日志有7个级别:SEVERE WARNING INFO CONFIG FINE FINER FINEST,默认为INFO级别。ALL代表所有级别,OFF表示关闭。
java.util.logging.Logger类
static Logger getLogger(String path.name.log)
static Logger getLogger(String name,String resName) 提供资源包的日志
void setLevel(Level l) 设置日志记录器级别
Level getLevel()
void addHandler(Handler h) 添加处理器
void removeHandler(handler h)
void setUseParantHandlers(bool flag) 设置是否使用父处理器
void log(Level level,String msg) 按指定级别记录日志
void log(Level level,String format,Object[] objs) 按指定级别记录日志
void logp(Level level,String classname,String methodName,String msg)
void logp(Level level,String classname,String methodName,String format,Object[] objs)
void logrb(Level level,String classname,String methodName,String resname,String format,Object[] objs)
void throwing(String classname,String methodName,Thowable t) 记录异常的信息
3.4 日志管理器配置
日志系统的配置默认位于jre/lib/logging.properties中。
如果要使用不同的配置,要在JVM启动时指定,java -Djava.util.logging.config.file = path MainClass
在配置文件中可以指定日志处理器、对日志处理器进行相应的配置、日志输出级别、日志输出格式等
3.5 日志的国际化
在获取日志时,还可以为日志指定资源包,以支持本地化的日志显示。
3.6 日志处理器
默认情况下,日志将记录发送到ConsoleHandler中,并由它输出到System.err流中。日志处理器也有级别,其只处理日志条目级别高于处理器级别的条目。
如果想绕过配置,则需要自己设置日志记录器的级别和处理器的级别,并将处理器安装到日志上。
如果希望将日志发送到其他地方,需要添加其他的处理器。系统提供了两个有用的处理器,FileHandler和Sockethandler。
用户可以自行继承自Handler或StreamHandler来实现自定义的日志输出方式。
java.util.logging.handler
abstract void publish(LogRecord lr) 将日志写到特定流
abstract void flush() 刷新已缓冲数据
Formatter getFormatter()
void setFormatter(Formatter f) 设置格式化器
void setLevel(Level l) 设置处理级别
java.util.logging.ConsoleHandler
java.util.logging.FileHandler
FileHandler(String namePatter,bool append) 以日志命名模式和是否追加初始化日志
FileHandler(String namePatter,int limit,int count,int append) limit控制日志内最大条数,count控制循环日志数
3.7 格式化器
系统提供的处理器可以生成平坦文本和XML格式的日志记录,我们也可以自定义格式,只需要扩展Formatter类并覆盖format()方法即可。对于XML格式化,需要覆盖getHead/Tail来提供特定的头和尾内容。
java.util.logging.Formatter
abstract String format(LogRecord lr)
String getHead/Tail()
String formatMessage(LogRecord lr)
4 使用断言
assert cond 如果cond为假,则抛出异常
assert cond:expr 如果cond为假,则使用expr作字符串构造异常
在默认时,断言并不生效,通过在控制台增加enable进行启用:java -enableassertion myapp
或java -ea:class1 -ea:packet1
启用和禁用断言并不需要重新编译程序,这是由类加载器来完成的。对于由虚拟机加载的类,可以使用esa来进行加载。
断言是一种在开发和测试中使用的技术,不要将断言代替异常和日志。
5 调试技术
5.1 常用方法
5.1.1 使用print或log方法输出程序信息到屏幕或文件中
5.1.2 在每个类中放置main方法,进行单元测试
5.1.3 使用JUnit进行单元测试
5.1.4 使用throwable提供的printStackTrace或thread.dumpStack跟踪执行堆栈
5.1.5 使用 -Xprof选项运行虚拟机,跟踪程序热点
5.2 图形程序测试
awt提供了Robot类,可以设置键盘和鼠标操作,并对UI进行测试。
5.3 使用调试器
5.3.1 JDB
一个类似于GDB的工具,可以对Java程序进行调试,不推荐使用。
5.3.2 Eclipse调试器
不在此描述