java 异常处理
1. 处理错误
异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器
需要关注的错误种类有:
- 用户输入错误
- 设备错误
- 物理限制
- 代码错误
1.1 异常分类
在java中,异常对象都是派生于Throwable类的一个实例。
派生于RuntimeException 的异常包含下面几种情况:
- 错误的类型转换。
- 数组访问越界;
- 访问null 指针
如果出现RuntimeException 异常, 那么就一定是你写的程序的问题” 是一条相当有道理的规则。
java将派生于Error类或RuntimeException类的所有一成称为非受查异常(unchecked)。所有的其他异常称为受查(checked)异常,编译器将核查是否为所有的受査异常提供了异常处理器。
1.2. 声明受查异常
在出现 下面的几种情况时应该抛出异常:
- 调用一个抛出受査异常的方法,
- 程序运行过程中发现错误, 并且利用throw 语句抛出一个受查异常
- 程序出现错误,
- Java 虚拟机和运行时库出现的内部错误。
对于那些可能被他人使用的Java 方法, 应该根据异常规范( exception specification), 在方法的首部声明这个方法可能抛出的异常。如果一个方法有可能抛出多个受查异常类型, 那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。但是, 不需要声明Java 的内部错误, 即从Error 继承的错误,同样,也不应该声明从RuntimeException 继承的那些非受查异常。
一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制( Error),
要么就应该避免发生( RuntimeException )。如果方法没有声明所有可能发生的受查异常, 编
译器就会发出一个错误消息。
如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用(也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常)。特别需要说明的是, 如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常。
1.3如何抛出异常
抛出异常的顺序:
- 找到一个合适的异常类
- 创建这个类的对象
- 将这个对象抛出
如:
1.4 创建异常类
在程序中, 可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。在这种情况下就有需要来创建自己的异常类了。我们需要做的只是定义一个派生于Exception 的类, 或者派生于Exception 子类的类。例如, 定义一个派生于IOException 的类。习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable 的toString 方法将会打印出这些详细信息, 这在调试中非常有用)。例如:
2. 捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息, 其中包括异常的类型和堆栈的内容。
捕获异常的try/catch语句块格式:
try{
code;
more code;
}catch(ExceptionType e){
handler for this Exception;
}
如果在try 语句块中的任何代码抛出了一个在catch 子句中说明的异常类, 那么程序将跳过try 语句块的其余代码。程序将执行catch 子句中的处理器代码。如果在try 语句块中的代码没有拋出任何异常,那么程序将跳过catch 子句。如果方法中的任何代码拋出了一个在catch 子句中没有声明的异常类型,那么这个方法就会立刻退出.通常,应该捕获那些知道如何处理的异常, 而将那些不知道怎样处理的异常继续进行传递。如果想传递一个异常, 就必须在方法的首部添加一个throws 说明符, 以便告知调用者这个方法可能会抛出异常。
再次抛出异常与异常链
将原始异常设置为新异常的原因:
try{
access the database;
}catch(SQLException e)
{
Throwable se = new ServletException('database error');
se.initCause(e);
throw se;
}
当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause() ;
finally子句
InputStream in = new FileInputStream(....);
try{
//1
code that might throw exceptions
//2
}catch(Exception e){
//3
handle the exception
//4
}finally{
//5
in.close()
}
//6
在上面这段代码中,有下列3 种情况会执行finally 子句:
- 代码没有抛出异常。在这种情况下, 程序首先执行try 语句块中的全部代码, 然后执行finally 子句中的代码。随后, 继续执行try 语句块之后的第一条语句。也就是说, 执行标注的1、2、5、6 处。
2 ) 抛出一个在catch 子句中捕获的异常。在上面的示例中就是IOException 异常。在这种情况下,程序将执行try 语句块中的所有代码,直到发生异常为止。此时,将跳过try 语句块中的剩余代码, 转去执行与该异常匹配的catch 子句中的代码, 最后执行finally 子句中的代码。如果catch 子句抛出了一个异常, 异常将被抛回这个方法的调用者。在这里, 执行标注1、3、5 处的语句。
3 ) 代码抛出了一个异常, 但这个异常不是由catch 子句捕获的。在这种情况下, 程序将执行try 语句块中的所有语句,直到有异常被抛出为止。此时, 将跳过try 语句块中的剩余代码, 然后执行finally 子句中的语句, 并将异常抛给这个方法的调用者。在这里, 执行标注1、5 处的语句。
4)try 语句可以只有finally 子句,而没有catch 子句。
try/catch语句块的推荐写法:
带资源的try语句
假设资源属于一个实现了AutoCloseable 接口的类,AutoCloseable 接口有一个方法:void close() throws Exception,在关闭资源时可以调用该方法对资源进行关闭。
try(Resource res = ..)
{
work with res
}
在try块退出时,会自动调用res.close()方法。也可以指定多个资源
try (Scanner in = new Scanne「(new FileInputStream('7usr/share/dict/words"). "UTF-8");PrintWriter out = new Pri ntWriter("out.txt"))
{
while (in.hasNextO)
out.pri ntl n(i n.next() .toUpperCaseO) ;
}
不论这个块如何退出, in 和out 都会关闭。如果你用常规方式手动编程, 就需要两个嵌套的try/finally 语句。
分析堆栈轨迹元素
堆栈轨迹(stack trace) 是一个方法调用过程的列表,它包含了程序了执行过程中方法调用的特定位置。
可以调用Throwable 类的printStackTrace 方法访问堆栈轨迹的文本描述信息
Throwable t = new Throwable();
StringWriter out = new StringWriter() ;
t.printStackTrace(new PrintWriter(out)) ;
String description = out.toString() ;
一种更灵活的方法是使用getStackTrace 方法, 它会得到StackTraceElement 对象的一个数组, 可以在你的程序中分析这个对象数组。例如:
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace() ;
for (StackTraceElement frame : frames)
analyze frame
StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法, 同时, 还含有能够获得类名和方法名的方法。toString 方法将产生一个格式化的字符串, 其中包含所获得的信息。
静态的Thread.getAllStackTrace 方法, 它可以产生所有线程的堆栈轨迹. 下面给出使用这个方法的具体方式:
Map<Thread, StackTraceElement[]> map = Thread.getAl1StackTraces() ;
for (Thread t : map.keySet () )
{
StackTraceElement[] frames = map.get(t) ;
analyze frames
}
3. 使用异常机制的技巧
-
异常处理不能代替简单的测试
与执行简单的测试相比, 捕获异常所花费的时间大大超过了前者, 因此使用异常的基本规则是: 只在异常情况下使用异常机制。
-
不要过分的细化异常
要把整个任务都放在异常机制块中,只要发生一个错误,整个任务就会取消。要将正常处理与错误处理分开。
-
要会利用异常层次结构
不要只抛出RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Thowable 异常, 否则,会使程序代码更难读、更难维护。、
考虑受查异常与非受查异常的区别。已检查异常本来就很庞大,不要为逻辑错误抛出这些异常
-
不要压制异常
如果编写了一个调用另一个方法的方法,而这个方法有可能100 年才抛出一个异常, 那么, 编译器会因为没有将这个异常列在throws 表中产生抱怨。而没有将这个异常列在throws 表中主要出于编译器将会对所有调用这个方法的方法进行异常处理的考虑。因此,应该将这个异常关闭;
-
在检测错误时,“苛刻”要比放任更好
当栈空时, Stack.pop 是返回一个null , 还是抛出一个异常? 我们认为:在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException 异常更好。
-
不要羞于传递异常
4. 使用断言
- 断言的概念
假设确信某个属性符合要求, 并且代码的执行依赖于这个属性。
assert 条件;or assert 条件:表达式;
这两种形式都会对条件进行检测, 如果结果为false, 则抛出一个AssertionError 异常。
在第二种形式中,表达式将被传人AssertionError 的构造器, 并转换成一个消息字符串。
- 启用和禁用断言
在默认情况下, 断言被禁用。可以在运行程序时用-enableassertions 或-ea 选项启用;选项-ea 将开启默认包中的所有类的断言。
- 使用断言完成参数检查
在java中,常用3种处理系统错误的机制:
- 抛出一个异常
- 写日志
- 使用断言
在使用断言时要注意:断言失败是致命的、不可恢复人错误。断言检查只用于开发和测试阶段。断言只应该用于在测试阶段确定程序内部的错误位置。
5. 记录日志
- 基本日志
要生成简单的日志记录,可以使用全局日志记录器(global logger) 并调用其info 方法:Logger.getClobal 0,info(“File->Open menu item selected”);
如果在适当的地方(如main 开始)调用Logger.getClobal () .setLevel (Level .OFF) ;将会取消所有的日志。 - 高级日志
日志记录器:限制要输出哪些日志记录语句,对日志信息进行级别限制。可以调用getLogger 方法创建或获取记录器:private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”) :
七级日志记录器级别:
- SEVERE
- WARNING
- INFO
- FINE
- FINER
- FINEST
设置日志的级别:logger.setLevel(Level.FINE);默认是记录前三个级别
也可以使用logger.warning(message)这种记录方法。还可以使用logger.log(Level.FINE,message);
**注意:**如果将记录级别设计为INFO 或者更低, 则需要修改日志处理器的配置。默认的日志处理器不会处理低于INFO 级别的信息
默认的日志记录将显示包含日志调用的类名和方法名, 如同堆栈所显示的那样。但是,如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用logp 方法获得调用类和方法的确切位置, 这个方法的签名为:void logp(Level 1, String className, String methodName, String message)
记录日志的常见用途是记录那些不可预料的异常。可以使用下面两个方法提供日志记录中包含的异常描述内容。
void throwing(St ring className, String methodName , Throwable t)
void log(Level 1 , String message, Throwable t)
日志管理器在VM 启动过程中初始化, 这在main 执行之前完成。如果在main中调用System.setProperty(“java.util_logging.config_file”,file), 也会调用LogManager.readConfiguration() 来重新初始化曰志管理器
- 调试技巧
- 在需要的地方使用println()和logger.getGlobal().info()方法打印出需要调试的变量的值。
- 在每个类中都放置一个单独人main方法,用这种方法对每个类进行单元测试。
- 使用JUnit框架来进行单元测试。
- 利用Throwable 类提供的printStackTmce 方法(多在catch语句块中使用),可以从任何一个异常对象中获得堆栈情况。
- 要想观察类的加载过程, 可以用-verbose 标志启动Java 虚拟机。
- 日志代理( logging proxy) 是一个子类的对象, 它可以截获方法调用, 并进行日志记录, 然后调用超类中的方法
- —般来说, 堆栈轨迹显示在System.err 上。也可以利用printStackTrace(PrintWriter s)
方法将它发送到一个文件中。 - -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检査。
- java 虚拟机增加了对Java 应用程序进行监控(monitoring) 和管理(management ) 的
支持。它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。 - 可以使用jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象。
- 如果使用-Xprof 标志运行Java 虚拟机, 就会运行一个基本的剖析器来跟踪那些代
码中经常被调用的方法。剖析信息将发送给System.out。输出结果中还会显示哪些方法是由
即时编译器编译的。