第11章 异常与调试
1. 处理错误
l 异常处理的任务就是要将控制权从产生错误的地方传给能够处理这种情况的错误处理器
l 异常分类:
异常对象都是派生于Throwable类的一个实例
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象
Exception分为IOException和RunTimeException
派生于RuntimeException的异常包含:
错误的类型转换
数组访问越界
访问空指针
派生于IOException的异常包括:
试图在文件尾部后面读取数据
试图打开一个错误格式的URL
试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在
如果出现RuntimeException 异常,就一定是你的问题。这是一道规则
Java语言规范将派生于RuntimeException类或Error类的所有异常称为未检查异常,其他的异常称为已检查异常。编译器将核查是否为所有已检查异常提供异常处理器。
l 声明已检查异常
方法应该在其首部声明所有可能抛出的异常,这样可以从方法首部反映出该方法可能会抛出哪类已检查异常
如果在方法执行期间,确实遇到这种糟糕的情况,构造器将不会初始化一个新的对象,而是抛出一个异常类对象
在遇到下面四种情况时才会抛出异常:
调用一个抛出已检查异常的方法
在程序运行过程中发生错误,并且利用throw语句抛出一个已检查异常
程序出现错误
Java虚拟机和运行时库出现的内部异常
任何一个抛出异常的方法都有可能是一个死亡陷阱,如果没有处理器捕获这个异常,当前执行的线程就会被中断
如果一个方法有可能抛出多个异常,就必须在方法的首部列出所有的异常类,每个异常类之间使用,隔开
不需要声明Java内部错误,就是从Error继承的那些异常。也不应该声明从RuntimeException继承的那些未检查异常
一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就要尽量避免其发生(RuntimeException)。
子类方法中声明的已检查异常不能超过超类方法中声明的异常范围。特别,如果超类方法没有抛出任何已检查异常,那么子类也不能抛出任何已检查异常
l 如何抛出异常
对于一个已经存在的异常类:
找到一个合适的异常类
创建这个类的一个对象
将对象抛出
l 创建异常类
定义一个派生于Exception的类,习惯上,定义的类应该包含两个构造器,一个是默认的构造器,另一个是带有详细描述信息的构造器
l
2. 捕获异常
l 捕获异常
如果某个异常发生的时候没有捕获它,程序就会终止,并在控制台上显示异常信息,其中包括异常的类型和堆栈的内容
要想捕获一个异常,必须设置try/catch语句快
如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法将立刻退出
通常,应该捕获那些知道如何处理的异常,而将不知道如何处理的异常传递出去。
使用捕获异常的特殊情况:如果编写一个覆盖超类方法的方法,而这个方法又没有抛出异常,那么就必须捕获方法代码中出现的每一个已检查异常
l 捕获多个异常
在一个try语句快中可以捕获多个异常类型,并对不同的类型的异常做不同的处理
l 再次抛出异常与链异常
在一个catch子句中,也可以抛出一个异常,这样做的目的是希望改变异常的类型
如果在一个方法中出现一个已检查异常,而不允许抛出它,那么,包装技术就十分有用, 我们可以捕获这个已检查异常,并将它包装成运行时异常
l Finally子句
当代码抛出一个异常时,就会终止对方法中剩余代码的处理,并退出这个方法。但不管是否有异常被捕获,finally子句中的代码都会被执行。
使用finally子句最常用的是数据库程序的关闭与数据库联接,已经多线程
发生下列三种情况会执行finally子句
代码没有抛出异常:程序首先执行try语句块中的全部代码,然后执行finally子句中的代码
代码抛出一个在catch子句中捕获的异常:跳过try语句块中剩余的代码,而转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码
代码抛出了一个异常,但这个异常不是由catch子句捕获的:程序将执行try语句块中的语句,直到有异常被抛出为止,此时将跳出try语句块中的剩余代码,然后执行finally子句中的代码,并将异常抛给这个方法的调用者
当finally子句包含return语句时,将会出现一种意想不到的结果。假设利用return语句从try块中退出。在方法返回之前,finally语句块的内容将会被执行,如果finally语句块中也包含一个return语句,那么这个返回值将会掩盖原始的返回值
有时在finally语句中的句子也会抛出异常,这样原始的异常将会丢失,转而抛出新的异常。所以,最好的方法是在finally子句中执行清除操作时不要抛出异常
l 堆栈跟踪
堆栈跟踪是一个方法调用过程的列表,它包含了程序执行中方法调用的特定位置。当Java程序正常终止,而没有捕获异常时,就会显示这个列表
3. 使用异常机制的建议
1) 异常处理不能代替简单的测试
与执行简单的测试相比,捕获异常所花费的时间大大超过了前者,因此使用异常的基本规则:只在异常情况下使用异常机制
2) 不要过分的细化异常
有时有必要将这个任务包装在一个try块中,这样,当任何一个操作出现问题时,整个任务就会被取消掉。
异常处理机制目标之一:将正常处理与错误处理分开
3) 利用异常层次结构:
不要只抛出RuntimeException异常。应该寻找更加适合的子类或创建自己的异常类
不要只捕获Throwable异常,否则,会使程序代码更加难度,更难维护。
已检查异常和未检查异常的区别:已检查异常本来就很庞大,这里不会抛出由于逻辑错误而造成的异常
将一种异常转换成另一种异常时不要犹豫
4) 不要压制异常:
5) 在检测错误时,苛刻要比放任更好:
在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException异常更好
6) 不要羞于传递异常:
很多程序员都感觉应该捕获抛出的全部异常,但如果调用一个抛出异常的方法,那么这些方法就会本能的捕获这些可能产生的异常。其实,传递异常要比捕获这些异常更好。
让高层次的方法通告用户发生了错误,或者放弃不成功的命令更加适宜
4. 记录日志
(省略 后面再学习)
5. 使用断言 assert x >= 0;
在一个具有自我防御能力的程序中,断言是一个常用的习语
断言仅仅使用在程序测试期间,断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检查语句将会自动被移走
表达式部分的唯一目的是产生一个消息字符串
l 启用和禁用断言
在默认情况下,断言是禁止的。可以在程序运行时使用-enableassertions或-ea启用它
在启用或者禁用断言时不必重新编译程序。启用或禁用断言时类加载器的功能。当断言被禁用时,雷加载器将跳过断言代码,因此,不会降低程序运行的速度
l 使用断言的建议
在Java语言中,给出了三种处理系统错误的机制:
抛出一个异常
日志
使用断言
什么时候应该使用断言:
断言失败是致命的不可恢复的错误
断言检查只用于开发和测试阶段
不应该使用断言向程序的其他部分通过发生了可恢复性的错误,或者不应该作为向程序用户通过问题的手段。断言之应该用于在测试间断确定程序内部的错误位置
l
l
6. 调试技术
1) 打印或记录变量的值
使用system.out.println(); 和 logger.global.info()
2) 在每个类中放置一个main方法,这样就可以对每个类进行单元测试了
只需要创建少量的对象,调用所有的方法,并检测每个方法是否能够正确地运行就可以了
3) Junit单元测试框架
4) 日志代理:是一个子类的对象,它可以窃取方法调用,并进行日子记录,然后调用超类
5) 利用Throwable类提供的printStackTrace方法,可以从任何一个异常对象中获得堆栈跟踪。
6) 一般来说,堆栈跟踪显示在System.err上,也可以利用voidprintStackTrace(PrintWriter s)方法将它发送到一个文件中
7) 通常将一个程序中的错误信息保存在一个文件中是非常有用的。
8) 要想观察类的加载过程,可以用-verbose标志运行Java虚拟机
9) 对于Swing窗口,按下CTRL+SHIFT+F1 键,将会按照层次结构打印出所有组件的信息
10) 如果自定义Swing组件,却不能正确地显示,就会发现Swing图形调试器(Swing graphics debugger)是一个非常不错的工具
11) -Xlint 选项,编译器可以对一些普遍出现的代码问题进行检查
12) Java应用程序进行检控和管理的支持,它允许利用虚拟机种的代理装置跟踪内存损耗、线程使用、类加载等情况
这个功能对于像应用程序服务器这样的大型的、长时间运行的Java程序 来说特别重要