`
bsr1983
  • 浏览: 1100860 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

第十一章 异常与调试

阅读更多
11.1 处理错误
异常处理的任务就是要将控制权从产生错误的地方传给能够处理这种情况的错误处理器。
11.1.1 异常分类
所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。
Exception层次结构又分解为两个分支:一个分支是派生于RuntimeException的异常;另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于RuntimeException;曾经能够正确运行,而由于某些情况(例如,I/O错误)导致的异常不属于RuntimeException。
“如果出现RuntimeException异常,就一定是你的问题”是一天相当有道理的规则。
Java语言规范将派生与RuntimeException类或Error类的所有异常称为“未检查(unchecked)异常”,其他的异常称为“已检查(checked)异常”。
11.1.2 声明已检查异常
如果遇到无法处理的情况,Java的方法可以抛出一个异常。
作为那些可能被他人使用的Java方法,应该根据“异常规范(exception specification)在方法的首部声明这个方法可能抛出的异常”。
如果一个方法有可能抛出多个已检查异常,就必须在方法的首部列出所有的异常类。每个异常类之间用逗号(,)隔开。
但是,不需要声明Java的内部错误,就是从Error继承的那些异常。任何程序代码都具有抛出这类异常的潜能,而我们对它们却没有任何控制能力。
同样,也不应该声明从RuntimeException继承的那些未检查异常。
总之,一个方法必须什么所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该避免它们的发生(RuntimeException)。如果方法没有声明所有可能发生的已检查异常,编译器就会给出一个错误消息。
警告:如果在子类中覆盖了超类的一个方法,那么,子类方法中声明的已检查异常不能超过超类方法中声明的异常范围。(也就是说,子类方法中抛出的异常范围更加小,或者根本不抛出任何异常。)特别需要说明的是,如果超类方法中没有抛出任何已检查异常,那么,子类也不能抛出任何已检查异常。
11.1.3 如何抛出异常
对于一个已存在的异常类,将其抛出非常容易:
1)找到一个合适的异常类。
2)创建这个类的一个对象。
3)将对象抛出。
11.1.4 创建异常类
定义一个派生于Exception的类,或者派生于Exception子类的类。习惯上,定义的类应该包含两个构造器,一个是默认的构造器,另一个是带有详细描述的构造器(超类Throwable的toString方法将会打印出这些详细信息,这对于调试代码来说是很方便的)。
11.2 捕获异常
如果某个异常发生的时候没有在任何地方捕获它,程序就会终止执行,并在控制台上显示异常信息,其中包括异常的类型和堆栈的内容。
要想捕获一个异常,必须设置try/catch语句块。try语句块的最简单形式如下:
try
{
code
more code
more code
}
catch(ExceptionType e)
{
handler for this type
}
如果try语句块中的任何代码抛出了一个在catch子句中指定的异常类,那么
1)程序将跳过try语句块中的其余代码。
2)程序将执行catch子句中的处理器代码
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
如果方法中的任何代码抛出了一个在catch中没有声明的异常类型,那么这个方法将立刻退出。
11.2.1 捕获多个异常
11.2。2 再次抛出异常与链异常
在一个catch子句中,也可以抛出一个异常,这样做的目的是希望改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么,用于标示子系统故障的异常类可能会产生多种解释。
在JDK1.4中,可以有一种更好的处理方法,并且将原始异常设置为新异常的“诱饵”:
try
{
access database
}
catch(SQLException)
{
Throwable se=new ServletException("database error")
se.setCause(e);
throw se;
}
当捕获到异常时,就可以使用下面这条语句得到原始异常:
Throwable e=se.getCause();
我们强烈建议使用这种包装技术。它允许用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
提示:如果在一个方法中出现一个已检查异常,而不允许抛出它,那么,包装技术就十分有用。我们可以捕获这个已检查异常,并将它包装成运行时异常。
注意:有些异常类,例如ClassNotFoundException、InvocationTargetException和RuntimeException,拥有它们自己的异常链方案。在JDK1.4中,这些已经引入了“诱饵”机制。然而,仍然可以利用原来的方式,或者调用getCause来得到异常链。遗憾的是,应用程序经常使用的异常链SQLException并没有给予修改。
11.2.3 finally子句
不管是否有异常被捕获,finally子句中的代码都会执行。
try语句可以只有finally子句,而没有catch子句。
提示:这里,强烈建议独立使用try/catch和try/finally语句块,这样可以提高代码的清晰度。例如:
InputStream in=...;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}
catch(IOException e)
{
show error dialog
}
在内层的try语句块只有一个职责,就是确保关闭输入流。在外层的try语句块也只有一个职责,就是保证保证报告中出现的错误。这种解决方案不仅清楚,而且还具有一个功能,就是报告finally子句中出现的错误。
警告:当finally子句中包含return语句是,将会出现一种意想不到的结果。假设利用return语句从try语句块中退出。在方法返回之前,finally语句块中的内容将被执行。如果finally语句块中也包含一个return语句,那么这个返回值将会掩盖原始的返回值。
11.2.4 堆栈跟踪元素分析
堆栈跟踪(stack trace)是一个方法调用过程的列表,它包含了程序执行中方法调用的特定位置。前面已经看到过这种列表,当Java程序正常终止,而没有捕获异常时,就会显示这个列表。
注意:堆栈跟踪只追踪抛出异常的语句,而不必跟踪错误的根源。
在JDK1.4以前的版本中,可以调用Throwable类的printStackTrace方法访问堆栈跟踪的文本描述。现在,可以调用getStackTrace方法获得一个StackTraceElement对象的数组,可以在程序中分析这个对象。
例如:
Throwable t=new Throwable();
StackTraceElement[] farames=t.getStackTrace();
for(StackTracelElement frame:frames)
analyze frame
StackTraceElement类含有获取文件名和当前执行代码的代码行号的方法,同时,还含有获取类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获取的信息。
在JDK5.0中,增加了静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈跟踪。下面给出使用这个方法的具体方式:
Map<Thread,StackTraceElement[]> map=Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
StackTraceElement[] frames=map.get(t);
analyze frames
}
11.2.5 Java错误与异常处理
11.3 使用异常机制的建议
使用异常机制的几点建议:
1)异常处理不能代替简单的测试
使用异常的基本规则:只有在异常情况下使用异常机制
2)不要过分喜欢异常
3)利用异常层次结构
不要只抛出RuntimeException异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Throwable异常,否则,会是程序代码更加难读、更难维护。
4)不要压制异常
5)在检测错误时,“苛刻”要比放任更好
6)不要羞于传递异常
注意:规则5、6可以归纳为“早抛出、晚捕获”
11.4 记录日志
11.4.1基本日志
日志系统管理着一个名为Logger,global的默认日志记录器,我们可以用它替换System.out,并通过调用info方法记录日志信息:
Logger.gloabl.info("File->Open menu item selected");
如果在恰当的地方(例如,main方法的开头)调用
Logger.gloabl.setLevel(Level.OFF);
则将会取消所有的日志。
11.4.2 高级日志
通常有以下7个日志记录器级别:
1)SEVERE
2)WARNING
3)INFO
4)CONFIG
5)FINE
6)FINER
7)FINEST
提示:默认的日志配置记录了INFO或更高级别的所有记录,因此,应该使用CONFIG、FINE、FINER和FINEST级别来记录那些有助于诊断而对于程序员没有太大意义的调试信息。
警告:如果将记录级别设置为INFO或者更低,则需要修改日志处理器的配置。默认的日志处理器不会处理低于INFO级别的信息。
11.4.3 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:
jre/lib/logging.properties
要想使用另一个配置文件,就要将java.util.logging.config.file特性设置为配置文件的存储位置,并用下列命令启动应用程序:
java -Djava.util.logging.config.file=configFile MainClass
警告:在main方法中调用System.setProperty("java.util.logging.config.file",
file);没有任何影响,因为日志管理器在虚拟机的启动过程中被初始化,它会在main之前执行。
警告:在日志管理器中配置的属性设置的不是系统属性,因此,用-Dcom.mycompany.myapp.level=FINE启动应用程序不会对日志记录器产生任何影响。
注意:日志属性文件是由java.util.logging.LogManager类处理的。可以通过将java.util.logging.manager系统属性设置为某个子类的名字来指定另一个不同的体制管理器。另外,在保存标准日志管理器的同时,还可以从日志属性文件跳过初始化。还有一种方式是将java.util.logging.config.class系统属性设置为某个类名,该类再通过其他方式日志管理器属性。
11.4.4 本地化
本地化的应用程序包含资源包中的本地说明信息。资源包由各个地区(例如,美国、德国)的映射集合组成。
11.4.5 处理器
在默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到System.err流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“”)有一个ConsoleHandler.
与日志记录器一样,处理器也有日志记录级别。对于一个要记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志记录器配置文件设置的默认控制台处理器的日志记录级别为
java.util.logging.ConsoleHandler.level=INFO
要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个很有用的处理器,一个是FileHandler,另一个是SocketHandler。SocketHandler将记录发送到特定的主机和端口。
11.4.6 过滤器
在默认情况下,过滤器根据日志记录的级别对它们进行过滤。每个日志记录器和处理器都有一个可选的过滤器来完成附件的过滤。另外,可以通过实现Filter接口并定义以下方法来自定义过滤器。
boolean isLoggable(LogRecord record)
注意:同一时刻最多只能有一个过滤器
11.4.7 格式化器
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但是,也可以自定义格式。这需要扩展Formatter类并覆盖下面的方法:
String format(LogRecord record)
11.5 使用断言
在一个具有自我防御能力的程序中,断言是一个常用的习语。
断言机制运行在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检查语句将会被自动地移走。
在JDK1.4中,Java语言引入了一个新关键字assert。这个关键字有两种形式:
assert 条件;

assert 条件:表达式
这两个语句都会对条件进行判定,如果结果为false,则抛出一个AssertionError异常。在第二个语句中,表达式将被传入AssertionError对象的构造器,并转换成一个信息字符串。
注意:“表达式”部分的唯一目的是产生一个消息字符串。AssertionError对象并不存储表达式的值,因此,不可能在以后对它进行查询。正如JDK文档所描述的那样,如果使用表达式的值,就会“鼓楼程序员试图从断言失败中恢复程序的运行,这不符合断言机制的初衷。”
如果使用JDK1.4,就必须告诉编译器正在使用assert关键字。此时需要使用-source 1.4选项,用法如下:
javac -source 1.4 MyClass.java
从JDK5.0开始,默认支持断言机制。
11.5.1 启用和禁用断言
默认情况下,断言是禁用的。可以在运行程序时用-enableassertions或-ea选项启用它:
java -enableassertions MyApp
需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器(class loader)的功能。当断言被禁用是,类加载器将跳过断言代码,因此,不会降低运行速度。
11.5.2 使用断言的建议
什么时候应该选择使用断言呢?请记住下面几点:
1)断言失败是致命的、不可恢复的错误。
2)断言检查只用于开发和测试阶段。
断言是一种测试和调试阶段所使用的战术性工具,而日志记录时一种在程序的整个生命周期都可以使用的策略性工具。
11.6 调试技术
11.6.1 调试的常用技巧
1)只要在代码的任何位置插入下面这条语句就可以获得堆栈跟踪:
Thread.dumpStack();
2)要想观察类的加载过程,可以用-verbose标志运行Java虚拟机。
3)如果曾经看到过Swing窗口,并对设计者能够采用某种管理手段将所有的组件排列整齐而感到好奇,就可以监视一下有关的信息。按下CTRL+SHIFT+F1,将会按照层次结构打印出所有组件的信息。
4)如果自定义Swing组件,却不能正确地显示,就会发现Swing图形调试器(Swing graphics debugger)是一个不错的工具。要想对一个Swing组件进行调试,可以调用JComponent类的setDebugGraphicsOptions方法。下面是调用这个方法的可选项:
DebugGraphics.FLASH_OPTION     在绘制每条线、每个矩形、每个文本之前,用红色闪烁显示
DebugGraphics.LOG_OPTION       打印每次绘制操作的信息
DebugGraphics.BUFFERED_OPTION  显示对显示区域之外执行的缓冲操作
DebugGraphics.NONE_OPTION      关闭图形调试
我们发现,要使闪烁选项能够工作,就必须禁用“双重缓冲”,这是Swing为缓解更新窗口时屏幕抖动现象所采取的一种策略。下面是用来开启闪烁选项的代码:
RepaintManager.currentManager(getRootPane()).setDoubleBufferingEnabled(false);
((JComponent)getContentPane()).setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION);
只要将这几行代码放置在框架构造器的尾部就可以了。当运行程序时,可以看到内容窗格以极慢的速度被填充。如果要进行本地化调试,只要为单个组件调用setDebugGraphicsOptions方法即可。对于这种闪烁,可以设置的参数包括持续时间、次数和闪烁的颜色。
5)JDK5.0增加类-Xline选项,这样,编译器可以对一些普遍出现的代码问题进行检查。例如,如果使用下面这条命令编译:
java -Xlint:fallthrough
那么,当switch语句中缺少break语句时,编译器就会给出报告(术语"lint"最初用来描述一种定位C程序中潜在问题的工具,现在通常用于描述查找可以的,但不违背语法规则的代码问题的工具)。
下面列出了可以使用的选项:
-Xlint或-Xlint:all     执行所有的检查
-Xlint:deprecation      与-deprecation一样,检查反对使用的方法
-Xlint:fallthrough 检查switch语句中是否缺少break语句
-Xlint:finally 警告finally子句不能正常地执行
-Xlint:none 不执行任何检查
-Xlint:path 检查类路径和源代码路径上的所有目录是否存在
-Xlint:serial 警告没有serialVersionUID的串行化类
-Xlint:unchecked 对通用类型域原始类型之间的危险转换给予警告
6)JDK5.0增加了对Java应用程序进行监控(monitoring)和管理(management)的支持,他允许利用虚拟机中的代理装置跟踪内存消耗、现场使用、类加载等情况。
JDK加载了一个称为jconsole的图形工具,它可以用于显示虚拟机性能的统计结果,要相对虚拟机进行监控,就需要使用-Dcom.sun.management.jmxremote选项启动虚拟机,然后,找出运行虚拟机的操作系统进程的ID。在UNIX/Linux环境下,运行ps实用工具;在Windows环境下,使用任务管理器。然后运行jconsole程序:
java -Dcom.sun.management.jmxremote MyProgram.java
jconsole processID
7)如果使用-Xprof标志运行Java虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。剖析信息将发送给System.out。输出结果中还会显示哪些方法是由just-in-time编译器编译的。
警告:编译器的-X选项并没有正式支持,而且在有些JDK版本中并不存在这个选项。可以运行命令java -X得到所有非标准选项的列表。
11.6.2 使用控制台窗口
11.6.3 跟踪AWT事件
11.6.4 AWT的Robot类
Java 2平台的1.3版本增加了一个Robot类,它用来将敲击键盘和点击鼠标的事件发送给AWT程序,并能够对用户界面进行自动检测。
11.7 使用调试器
11.7.1 JDB调试器
JDK包含的JDB是一个非常原始的命令行调试器。
11.7.2 Eclipse调试器
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics