版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。
第7章 异常、断言和日志
作者:ExcelMann,转载需注明。
第7章内容目录:
- 处理错误
- 捕获异常
- 使用异常的技巧
- 使用断言
- 日志
- 调试技巧
一、处理错误(声明异常->抛出异常)
异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器。为了能够处理程序中的异常情况,必须考虑到程序中可能出现的错误和问题。
在Java中,如果某个方法不能够采用正常的途径完成它的任务,可以通过另外一个途径退出方法。在这种情况下,方法并不返回任何值,而是抛出一个封装了错误信息的对象。
需要注意的是,在这种情况下,方法的代码不会继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常情况的异常处理器。
1、异常分类
- 异常对象都是派生于Throwable类的一个类实例。(也可以创建自己的异常类)
- Error类层次结构:该层次结构描述了Java运行时系统的内部错误和资源消耗情况。(这种情况很少见)
- Exception类层次结构:该层次结构分为两个分支,一个分支派生于RuntimeException,另一个分支包含其他异常;
- 派生于RuntimeException的异常包括以下问题(通常属于编程错误,自己的问题):
1)错误的强制类型转换;
2)数组访问越界;
3)访问null指针; - 不是派生于RuntimeException的异常包括:
1)试图超越文件末尾继续读取数据;
2)试图打开一个不存在的文件;
3)试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在; - 检查型和非检查型异常:派生于Error类或RuntimeException类的所有异常成为非检查型异常,所有其它的异常通常称为检查型异常;
(注意:编译器将检查你是否为所有的检查型异常提供了异常处理器)
2、声明检查型异常
- 声明检查型异常的目的:告诉编译器这个方法中的代码可能会发生什么错误,让方法的调用者去操心这个问题;
- 一个方法必须声明所有可能抛出的检查型异常;
- 当代码真的抛出一个异常类对象时,运行时系统就会开始搜索知道如何处理这个异常类对象的异常处理器;
- 需要记住在遇到下面4种情况时会抛出异常(前两种需要主动抛出检查型异常,告诉调用者可能抛出异常):
1)(★常见)调用了一个抛出检查型异常的方法;
2) 检测到一个错误,并且利用throw语句抛出一个检查型异常;
3) 程序出现错误,例如下标溢出;
4) Java虚拟机或运行时库出现内部错误; - (★)父类与子类的异常:
1) 如果在子类中覆盖了父类的一个方法,子类方法中声明的检查型异常不能比超类方法的声明异常更通用;(可以抛出更特定的、直接不抛出异常)
2) 如果超类方法没有抛出任何检查型异常,子类也不能抛出任何检查型异常;
3、如何抛出异常
Java中采用逐层向上抛异常的机制,直到异常遇到catch子句。当前方法若将异常throw抛出,则会往上抛给该方法的调用者。
抛出异常步骤如下:
1)找到一个合适的异常类(也可以自己创建异常类)
2)创建这个类的一个对象
3)将对象抛出
例子:假设正在读取一个文件,文件的承诺长度为1024个字符,然而,当读到733个字符之后文件就结束了,此时希望抛出一个异常。
String readData(Scanner in) throws EOFException //抛出异常,因为下面throw异常对象
{
while(...)
{
if(n < len)
throw new EOFException(); //创建该类的一个对象,并将对象抛出
}
}
4、创建异常类
- 自定义的异常类应该派生于Exception、或者Exception的某个子类;
- 自定义的这个类应该包含两个构造器,一个是默认的构造器,另外一个是包含详细描述信息的构造器(超类Thorwable的toString方法会返回一个字符串,包含这个详细信息;getMessage方法会直接返回详细信息)
例子:
class FileFormatException extends IOException
{
public FileFormatException(){}
public FileFormatException(String gripe){
super(gripe);
}
}
二、捕获异常
1、捕获异常
- try/catch语句块:想要捕获一个异常,需要设置try/catch语句块。当try语句中的代码抛出了catch子句中指定的异常时,则会停止try的剩余代码运行,跳转到catch子句;
- 什么时候要捕获,什么时候要抛出?
一般情况下,如果你知道如何处理这个异常,那么你可以选择捕获该异常;
否则,通常最好的选择是什么都不做,将该异常抛出,交个它的调用者来处理;
特殊情况:当父类未声明异常时,子类中必须捕获!因为子类的throws不能比父类的更通用; - 注意:编译器将严格执行throws说明符。如果你调用了一个抛出检查型异常的方法,要么选择处理这个异常,要么选择继续抛出该异常;
2、捕获多个异常
- 在一个try语句块中可以捕获多个异常类型,要为每个异常类型使用一个单独的catch子句(除了Java7之后);
- 在Java7中,同一个catch子句可以捕获多种类型的异常(只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性);
(注意:捕获多个异常时,异常变量隐含为final变量)
3、再次抛出异常与异常链
再次抛出异常的意思是,在catch子句中,再次将捕获的异常抛出,通常的目的是希望改变异常的类型。
通常的做法:采用“包装技术”,例子如下。这样在子系统中抛出高层异常,而不会丢失原始异常的细节;
//包装技术:
try{
accesst the database
}
catch(SQLException original)
{
var e = new ServletException("database error");
e.initCause(original);
throw e; //最终抛出的异常是ServletException类型,声明也得是这种类型
}
//捕获到该异常时,可以使用下面语句获取原始异常
Throwable original = caughtException.getCause();
4、finally子句
finally子句的目的:是为了清理资源,不过其实后面介绍的try-with-resources语句比finally子句更加常用。所以不要将任何改变控制流的语句(return、throw、break、continue)放在finally中;
例子:
这个程序可能在以下3种情况下执行finally子句
1)代码没有抛出异常。则执行顺序是:1、2、5、6;
2)代码抛出一个异常,并在一个catch子句中捕获。此时,如果catch子句中没有抛出异常,那么执行顺序是:1、3、4、5、6;如果catch子句中抛出了异常,即将异常交给调用者处理,那么执行顺序是:1、3、5;
3)代码抛出了异常,但是没有catch子句捕获到该异常,最后执行完finally语句后,异常会抛回调用者。此时执行顺序是1、5;
var in = new FileInputStream(...);
try{
///1
code that might throw exceptions
///2
}
catch(IOException e)
{
///3
show erroe message
///4
}
finally{
///5
in.close();
}
///6
写法改进:采用两层try,分配职责。内层的try语句的职责是确保关闭输入流,外层的try语句负责捕获异常,报告出现的错误;(好处:外层的try语句可以捕获内层的finally可能出现的错误)
InputStream in = ...;
try{
try{
code that might throw exceptions
}
finally{
in.close();
}
}
catch(Exception e)
{
...
}
注意点:
- 不管是否有捕获到异常,finally子句的代码最终都会执行;
- try语句可以只有finally子句,而没有catch子句;
- 当finally子句中包含有return语句时,该return会覆盖try的return语句,甚至忽略try语句抛出的异常,出现问题!
5、try-with-Resource语句
- try-with-Resource语句的最简形式是:
try(Resource res = ...; Resouce res1 = ...;) //可以指定多个资源
{
work with res and res1
}
当try块正常退出或者存在异常时,都会自动调用res.close();
- 在Java9中,可以在try首部中提供之前声明的事实最终变量;
- 当try块和close方法都抛出一个异常时,解决方法是:在try-with-Resources中,try中的异常会重新抛出,而close方法抛出的异常会被“抑制”且自动捕获,通过addSuppressed方法添加到try的异常中。如果你想取出close方法抛出的异常,可以调用getSuppressed方法;
- try-with-resources语句自身可以有catch子句,甚至还可以有finally子句。这些子句会在关闭资源之后执行;
6、分析堆栈轨迹元素(待深入学习。。)
堆栈轨迹是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。
访问堆栈轨迹的方式(3种):
1)调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息;
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
2)更灵活的方式:使用StackWalker类,生成一个StackWalker.StackFrame实例流,其中每个实例分别描述一个栈帧。利用迭代处理这些栈帧:
StackWalker walker = StackWalker.getInstance();
walker.forEach(farame -> analyze frame)
3)懒方式:以懒方式处理Stream<StackWalker.StackFrame>
walker.walk(stream -> process stream)
三、使用异常的技巧
- 异常处理不能代替简单的测试。因为捕获异常所花费的时间大大超过简单的测试,所以使用异常的规则是:只在异常情况下使用异常;
- 不要过分地细化异常。意思是,不要将每一条语句都分装在一个独立的try语句块中,而是将正常处理和异常处理分开。
- 充分利用异常层次结构:
1)不要只抛出RuntimeException异常。应该寻找一个适合的子类或者自己创建异常类;
2)不要只捕获Throwable异常。考虑检查型异常和非检查型异常的区别;
3)如果能够将一种异常转化为另一种更适合的异常,那么不要犹豫; - 不要压制异常。若觉得该异常很重要且你可以处理,那么就捕获处理;
- 在检测错误时,苛刻要比放任更好。当有问题时,该需要抛出异常就抛出;
- 不要羞于传递异常。最好是传递异常,而不是自己捕获。因为更高层的方法通常可以更好地通知用户发生了错误,或者放弃不成功的命令;
四、使用断言
1、断言的概念
- 断言机制:允许在测试期间向代码插入一些检查,而在生产代码中会自动删除这些检查;
- 关键字assert:断言的两种形式
1) assert condition
2) assert condition : expression;
这两个语句都会计算condition条件,如果结果为false,则抛出一个AssertionError异常。在第二个语句中,表达式将传入AssertionError对象的构造器,并转换为一个消息字符串。(注意:表达式的唯一目的是产生一个消息字符串。如,assert x>=0:x;是将x的实际值传给AsserttionError对象,以便以后显示)
2、启用和禁用断言
- 在默认情况下,断言是禁用的。可以在运行程序时用-enableassertions或-ea选项启用断言: 如java -enableassertions MyApp;
- 使用选项-deableassetions或-da在某个特定类或包中禁用断言;
- -ea和-da开关不能应用到那些没有类加载器的“系统类”上。对于系统类,要使用-enablesystemassetions/-esa开关启用断言;
- 注意:启用或禁用断言是类加载器的功能。禁用断言时,类加载器会去除断言代码,因此不会降低程序运行速度;也不必重新编译程序来启用或禁用断言;
3、使用断言完成参数检查
- 对什么参数检查?一般指方法文档中有“前置条件”,且无需抛出异常的参数。
- 什么时候应该选择断言呢?
1)断言失败是致命的、不可恢复的错误;
2)断言检查只是在开发和测试阶段打开(确定程序内部错误的位置); - 前置条件的例子:@param a the array to be sorted (must not be null).
- 当有断言的方法被非法调用时,它的行为是难以预料的,取决于类加载器的配置;
五、日志
待深入学习。。。
六、调试技巧
像Eclipse等专业集成开发环境都提供了调试器,供开发者很好的调试代码。不过,本节内容给出一些使用调试器之前,应该学会的一些调试技巧。
- 可以用下面的方法打印或记录任意变量的值;
System.out.println(x); Logger.getGlobal().info(x); - 可以在每一个类中放置一个单独的main方法。这样就可以提供一个单元测试桩,能够单独测试类。
- 对于第2个技巧,可以使用JUnit框架。这是一个流行的单元测试框架。只要对类进行了修改,就需要运行测试。
- (待学习)日志代理是一个子类的对象,它可以截获方法调用,记录日志,然后调用超类中的方法。
例子:如果在调用Random类的nextDouble方法出现了问题,那么可以创建一个代理对象,只要调用了nextDouble方法,就会生成一个日志对象。
var generator = new Random()
{
public double nextDouble()
{
double result = super.nextDouble();
Logger.getGlobal().info("nextDouble+" + result);
return result;
}
}
- 通过打印堆栈轨迹来分析。一般,可以通过捕获异常,打印出堆栈轨迹,然后再抛出异常的方式。不过也可以直接在代码的某个位置调用以下命令打印出堆栈轨迹,Thread.dumpStack();
- 一般堆栈轨迹会显示在System.err上,如果想要记录或者显示堆栈轨迹,可以如下将它捕获到一个字符串中:
var out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String description = out.toString();
- (不太理解)通常,将程序错误记入一个文件会很有用。不过,错误会发送到System.err,而不是System.out。
- (不太理解)对于未捕获的异常的堆栈轨迹,可以将这些消息记录到一个文件中。可以用静态方法Thread.setDefaultUncaughtExceptionHandler改变未捕获异常的处理器。
- 想要观察类的加载过程,启动Java虚拟机时可以使用-verbose标志。
- -Xlint选项告诉编译器找出常见的代码问题。例如,如果使用下面这条命令编译:
javac -Xlint sourceFiles
当switch语句中缺少break语句时,编译器会报告这个问题。 - jconsole工具:Java虚拟机增加了对Java应用程序的监控和管理支持,允许在虚拟机中安装代理来跟踪内存消耗、线程使用、类加载等情况。
- Java任务控制器:是一个专业级性能分析和诊断工具,包含在Oracle JDK中,可以免费用于开发。