Java错误处理:
在计算机运行过程中,错误总是会出现:
用户输入的错误;
读写文件的错误;
网络错误,内存耗尽等等。
如果一个方法调用出错,调用方如何得知这个错误呢?
异常:
Java使用异常来表示错误。异常是class,本身带有类型信息。异常可以在任何地方抛出。异常只需要在上层捕获,和方法调用分离。
如:
Java的异常体系:
Java规定,必须捕获的异常包括Exception及其子类,但不包括RuntimeException及其子类。
我们把Exception及其子类称为Checked Exception。
不需要捕获的异常包括Error及其子类,RuntimeException及其子类。
因为Error是发生了严重错误,程序一般对此无能为力。而Exception是发生了运行时的逻辑错误,应该捕获异常并处理。
捕获并处理错误:IOException,NumberFormatException;
修复程序:NullPointerException,IndexOutOfBoundsException。
我们使用try...catch捕获异常,可能发生异常的语句放在try{...}中,使用catch捕获对应的Exception及其子类。
如:
对可能抛出Checked Exception的方法调用,我们可以捕获Exception处理,如果不捕获则可以用过throws声明,但通过throws声明后仍然需要在上层捕获。main()方法是最后捕获Exception的机会。如果仍没有捕获,JVM就会报错并退出。
如:
总结:
Java使用异常来表示错误,并通过try{...}catch{...}捕获异常;
Java的异常是class,并且从Throwable继承;
Error是无需捕获的严重错误;
Exception是应该捕获的可处理的错误;
RuntimeException无需强制捕获,非RunimeException(Checked Exception)需强制捕获,或者用throws声明。
捕获异常:
我们可以通过try{...}catch{...}捕获异常。
还可以使用多个catch子句来捕获不同的Exception及其子类。
如:
catch捕获时可以用或操作符“|”在一条catch子句中同时捕获多种错误。
如:
写的时候注意catch的顺序,子类必须写前面,因为是按从上到下的顺序捕获的。如果父类在前,由于某个子类也属于这个父类,那么就会先被父类的catch给捕获。
如:
finally语句可以保证有无错误都会执行。finally语句不是必须的,finally总是最后才执行。
如:
总结:
catch子句的匹配顺序非常重要,子类必须放前面;
finally子句保证有无异常都会执行,finally是可选的;
catch可以匹配多个非继承关系的异常(JDK>=1.7)。
抛出异常:
当某个方法抛出异常时,如果当前方法没有捕获,异常就会被抛到上层调用方法,直到遇到某个try...catch被捕获。
如:
printStackTrace()可以打印出方法的调用栈,对于调试错误非常有用。
如:
这样的就是方法调用栈。从中我们可以一步一步找出异常最初是从哪里来的。
观察调用栈,我们可以找出异常是从哪里被抛出的。
如何抛出异常?
创建某个Exception的实例,用throw语句抛出。
如:
转换异常:
如果一个方法捕获了某个异常后,又在catch中抛出新的异常,就相当于把抛出的异常类型给“转换”了。
如:
新的异常会丢失原有的异常信息,我们可以让新的Exception持有原始异常信息。
如:
注意:
在抛出异常前,finally语句会保证执行。如果finally语句抛出异常,则catch语句将要抛出的异常不再执行。
如:
箭头所指的没有被抛出的异常称为“被屏蔽”的异常(suppressed exception)。
如何保存所有的异常信息?
我们可以用origin变量保存原始异常;如果存在原始异常,用addSuppressed()添加新异常;如果存在原始异常,或者新异常,最后在finally抛出。
如:
我们可以用getSupperssed()获取所有的Suppressed Exception。
如:
总结:
printStackTrace()可以打印异常的传播栈,对调试很有用;
捕获异常并再次抛出新的异常时,应该持有原始异常信息;
如果在finally中抛出异常,应该把新抛出的异常添加到原有异常中;
用getSuppressed()可以获取所有添加的Supperssed Exception;
处理Suppressed Exception要求JDK>=1.7。
自定义异常:
首先看看JDK已有的异常:
我们还可以定义新的异常类型,新的异常类型可以从合适的Exception派生,推荐从RuntimeException派生,这样我们就不需要强制捕获自定义的异常。也不需要在方法中声明需要抛出异常。
我们可以定义新的异常关系树:从适合的Exception派生BaseException,其他Exception从BaseException派生。
如:
自定义的异常应该提供多个构造方法。
总结:
自定义异常应该从合适的Exception派生;
推荐用RuntimeException;
自定义异常应该提供多个构造方法;
可以使用IDE根据父类快速创建构造方法。
Java断言和日志:
断言:
断言(Assertion)是一种程序调试的方式,使用assert关键字,断言条件预期为true,如果断言失败,抛出AssertionError。我们还可以设置一个断言消息。
如:
断言的特点:
断言失败时会抛出AssertionError,导致程序结束退出;
不能用于可恢复的程序错误(可恢复的程序错误不应该使用断言);
只应该用于开发和测试阶段。
JVM默认关闭断言指令:
给Java虚拟机传递-ea参数启用断言;
可以指定特定的类启用断言;
可以指定特定的包启用断言。
总结:
断言是调试方式,断言失败会抛出AssertionError;
只能在开发和测试阶段启用断言;
对可恢复的错误不能使用断言,应该抛出异常;
断言很少被使用,更好地方法是编写单元测试。
日志:
日志(logging)是为了取代System.out.println()语句。
日志可以设置输出样式;可以设置输出级别,禁止某些级别输出;可以被重定向到文件;可以按包名控制日志级别。
JDK内置了Logging(即JDK Logging),在java.util.logging包中。
如:
JDK Logging定义了7个日志级别:
如果设置为INFO,则输出INFO以上的3个级别的日志。
JDK Logging的局限:
JVM启动时读取配置文件并完成初始化;
JVM启动后无法修改配置;
需要在JVM启动时传递参数-Djava.util.logging.config.file=config-file-name。
总结:
日志是为了替代System.out.println(),可以定义格式,重定向到文件等;
日志可以存档,便于追踪问题;
日志记录可以按级别分类,便于打开或者关闭某些级别;
可以根据配置文件调整日志,无需修改代码;
JDK提供了Logging:java.util.logging。
Commons Logging:
Commons Logging是Apache创建的日志模块:
可以挂接不同的日志系统;
可以通过配置文件指定挂接的日志系统;
自动搜索并使用Log4j;
使用JDK Logging(JDK>=1.4)。
如:
Commons Logging定义了6个日志级别:
如何在Eclipse中引入jar包:
Apache Commons Logging的包下载地址:http://commons.apache.org/proper/commons-logging/
Project -> Property -> Java Build Path -> Libraries -> Add Jars...
建议在工程目录下创建一个lib文件夹,把所有引用的jar包都放到这个文件夹内。
初始化Log对象:
final Log log = LogFactory.getLog(getClass());
总结:
Commons Logging是使用最广泛的日志模块;
Commons Logging的API非常简单;
Commons Logging可以自动使用其他日志模块。
Log4j:
Log4j是目前最流行的日志框架。目前已有2.X版本。
Appender可以把同一个log输出到不同的目的地:Console(屏幕)、File、Socket(远程);Filter是过滤器,决定哪些log需要被输出;Layout用来格式化log信息。
Commons Logging如果在classpath中发现了log4j,就会使用log4j。
最佳使用方法:
使用Commons Logging接口来写入日志;
开发阶段无需引用Log4j;
使用Log4j只需要把正确的配置文件和相关jar包放入工程的classpath中;
使用配置文件可以灵活修改日志的输出,无需修改代码。
总结:
通过Commons Logging实现日志,不需要修改代码即可使用Log4j;
使用Log4j只需要把log4j2.xml和相关jar放入classpath;
如果要更换Log4j,只需要移除log4j2.xml和相关jar;
只有扩展Log4j时,才需要引用Log4j的接口。