异常描述
1.简介
为了全面了解“异常”的概念,先来分析一个实例。假定要编写一个Java程序,该程序读取用户输入的一行文本,并在终端显示该文本。这里是一个演示Java语言I/O功能的简答回显(echo)程序。如果认为代码一定能正常运行,则可以编写一下程序。
import java.io.*;
public class EchoDemo{
public static void main(String[] args){
System.out.println("Enter text to echo");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader inputReader = new BufferedReader(isr);
String inputLine = inputReader.readLine();
System.out.println("READ:\t"+inputLine);
}
}
分析这段代码,在EchoDemo类中,第3行声明了一个main方法;第4行提示用户输入文本;在第5、6行设置BufferReader对象连接到InputStreamReader,而InputStreamReader又连接到标准输入流System.in;第7行读入一行文本;第8行用标准输出流System.out显示出该文本。
实际上EchoDemo类是完全有可能出现问题。要调用readLine方法的时候要正确读取输入,下面几种假设都必须成立:
- 1.假定键盘有效,键盘能与计算机正常通信;
- 2.假定键盘数据可从操作系统传输到Java虚拟机,又从Java虚拟机传输给inputReader。
JDK执行结果
readLine方法有事会产生IOException。编译器要求“捕获”或“声明”IOException。
- “捕获(catch)”指当readLine方法产生错误时截获该错误,并处理或记录问题
- “声明(declare)”指错误可能引发IOException,并通知调用该方法的任何代码:可能产生异常。
捕获异常
import java.io.*;
public class EchoDemo{
public static void main(String[] args){
System.out.println("Enter text to echo");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader inputReader = new BufferedReader(isr);
try{
String inputLine = inputReader.readLine();
System.out.println("READ:\t"+inputLine);
}catch(IOException e){
System.out.println("Exception encountered:" + e);
}
}
}
新添加的代码块包含关键字try和catch,表示如果要读取输入成功则正常运行,如果读取输入失败则捕获问题(由IOException对象表示),并采取相应的措施。
声明异常
import java.io.*;
public class EchoDemo{
public static void main(String[] args) throws IOException{
System.out.println("Enter text to echo");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader inputReader = new BufferedReader(isr);
String inputLine = inputReader.readLine();
System.out.println("READ:\t"+inputLine);
}
}
此代码中未采取措施来处理readLine方法的可能故障,但声明:main方法可能产生IOException,调用该方法的任何代码都必须做好处理问题的准备。
JDK执行结果
2.异常概念
Java等语言的大多数应用程序始终在使用消息。在GUI中,消息经常被用作发送事件信息(用户与应用程序交互的一些相关数据类型,如单击按钮,或在文本框中输入文本)的方式。
从某种意义上说,异常就是这样的一种消息:她传递一些系统问题、故障及未按规定执行的动作的小官信息。异常包含信息,以将信息从应用程序的一部分发送到另一部分。
3.异常类的层次结构
在Java中,所有异常有一个共同祖先Throwable(可抛出)。Throwable指定代码中可用异常传播机制通过Java应用程序传输的任何问题的共性。
Throwable有两个重要的子类:Exception(异常)和Error(错误),二者都是Java异常处理的重要子类,各自都包含大量子类。
- “异常”是应用程序中可能的可预测、可恢复问题。Java API文档记录给出的定义是:“合理应用程序可能需要捕获的情况。”一般地,大多数异常表示轻度到中度的问题。
- “错误”表示运行应用程序中的比较严重问题。Java API文档记录给出的定义是:“是Throwable的一个子类,代表严重问题,合理应用程序不应该试图捕获。大多数此类错误属反常情况。”
Java中的异常可以分为两种异常类型--受检异常(checked exception)和非受检异常(unchecked exception)。非受检异常指的是java.lang.RuntimeException和java.lang.Error类及其子类,其他所有的异常类都被称之为受检异常。两种类型的异常在作用上没有差别,唯一的差别是使用受检异常时的合法性要在编译时刻由编译器来检测,因此受检异常在使用的时候需要比非受检异常更多的代码来规避编译错误。
一直以来,关于在程序中到底是该使用受检异常还是非受检异常,开发者之间一直存在着争议,毕竟两者各有优缺点。受检异常的特点在于它强制要求开发者在代码中进行显式的声明和捕获,否则就会产生编译错误。这种限制从好的方面来说,可以防止开发者意外地忽视某些出错的情况,因为编译器不允许出现未被处理的受检异常;从不好的方面来说,受检异常对程序中的设计提出了更高的要求。不恰当的使用受检异常,会使代码中充斥着大量没有实际作用、只是为了通过编译而添加的代码。而非受检异常的特点是,如果不捕获异常,不会产生编译错误,异常会在运行时刻才被抛出。非受检异常的好处是可以去掉一些不需要的异常处理代码,而不好之处是开发者可能忽略某些应该处理的异常。
目前的主流意见是,最好优先使用非受检异常
4.异常的处理或声明
4.1 处理异常:try、catch和finally
如果想要捕获异常,则必须在代码中添加异常处理器块。这种Java结构可能包含3部分,都有Java关键字。
示例:
import java.io.*;
public class EchoDemo{
public static void main(String[] args){
System.out.println("Enter text to echo");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader inputReader = new BufferedReader(isr);
try{
String inputLine = inputReader.readLine();
System.out.println("READ:\t"+inputLine);
}catch(IOException e){
System.out.println("Exception encountered:" + e);
}finally{
System.out.println("My job here is done");
}
}
}
try块
在将一个或者多个语句放入try时,则表示这些语句可能抛出异常。编译器知道可能要发生异常,于是用一个特殊结构评估块内所有语句。
catch块
当问题出现时,一种选择是定义代码块来处理问题,catch块的目的便在于此。catch块是try块所产生异常的接受者。基本原理为:一旦生成异常,则try块的执行终止,JVM将查找相应的catch块。
finally块
还可以定义这样一个代码块,无论试图运行try块代码的结果如何,该块一定运行。finally块的作用便在于此。在常见的所有环境中,finally块都将运行。无论try是否运行完,无论是否产生异常,也无论异常是否在catch块得到处理,finally都将运行。
4.2 try-catch-finally的规则
- 必须在try之后添加catch或finally块。try块后可同时接catch和finally,且至少有一个块。
- 必须遵守块的顺序:若代码同时使用catch和finally块,则必须将catch块放在try块之后。
- try块与相应的catch或finally之间可能不存在语句。
- catch块与特定异常的类型相关。
- 一个try块可能有一个或者一个以上的catch块。如果是一个以上的catch块的话,将执行第一个匹配块。有一个经验法则:要按照从最具体到最一般的顺序组织处理块。
- 除下列情况,总将执行finally作为结束:
a. JVM过早终止(调用System.exit(int))
b. 在finlly块中抛出一个未处理的异常
c. 计算机断电、失火,或遭遇病毒攻击 - 可嵌套try-catch-finally结构。
- 在try-catch-finally结构中可以重新抛出异常。
4.3 声明异常
若要声明,则必须将其添加到方法签名块的结束位置,即输入部分之后。
示例:
public void errorProneMethod(int input) throws java.io.IOException{
//Code for the method,including one or more method
//calls that may produce an IOException
}
这样,声明的异常将传给方法调用者,而且也通知了编译器:该方法的任何调用者必须遵守处理或声明规则。
4.4 声明异常的规则
- 必须声明方法可能抛出的任何可检测异常(checked exception)。
- 非检测异常(unchecked exception)不是必需的,可声明,也可不声明。
- 调用方法必须遵守任何可检测异常的处理或声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
4.5 异常的API
实际上,Java的Exception和Errors的所有行为都集中在Throwable类中。
其代码(节选)如下
public class Throwable implements Serializable {
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -3042686055658047285L;
/**
* The throwable that caused this throwable to get thrown, or null if this
* throwable was not caused by another throwable, or if the causative
* throwable is unknown. If this field is equal to this throwable itself,
* it indicates that the cause of this throwable has not yet been
* initialized.
*
* @serial
* @since 1.4
*/
private Throwable cause = this;
/**
* Specific details about the Throwable. For example, for
* {@code FileNotFoundException}, this contains the name of
* the file that could not be found.
*
* @serial
*/
private String detailMessage;
}
说明:
- Throwable类定义实现Serializable接口,这意味着,Java的每个Exception类和Error类也实现Serializable接口。
- Throwable存储3个属性:Message(消息)、Stack Trace(栈跟踪)和Cause(原因)。
名称 | 类 | 描述 | 读-写 |
---|---|---|---|
Message | String | 描述性文本.详细介绍异常 | 读(只能在构造函数中设置) |
Stack Trace | StackTraceElement[] | 引发异常的所有方法调用的记录 | 读-写 |
Cause | Throwable | 产生此异常的起因 | 读-写(只能设置一次) |
Message进一步描述与异常相关的问题,可在两个构造函数中设置,可调用getMessage方法检索它
Stack Trace提供与异常相关的调用栈的记录--应用程序代码中特定异常导致代码停止执行的一系列位置。栈跟踪详细记录在抛出异常时执行停止的应用程序各点。
Cause是JDK1.4引入的新特性。这样,可以将另外一个Throwable对象指定为当前异常的“起因”。getCause和initCause方法提供了获取和设置此属性的方法。
5.异常声明是API的一部分
主要是针对受检异常的。在一个公开方法的声明中使用throws关键词来声明其可能抛出的异常的时候,这些异常就成为这个公开方法的一部分,属于开放API。在维护这个公开API的时候,这些异常有可能会对API的演化造成阻碍,使得编写代码时不能不考虑向后兼容性的问题。
如果公开方法声明了会抛出一个受检异常,那么这个API的使用者肯定已经使用了try-catch-finally来处理这个异常。如果在后面的版本更新中,发现该API抛出这个异常是不合适的,也不能直接把这个异常的声明删除。因为这样会造成之前的API使用者的代码无法通过编译。
因此,对于API的设计者来说,谨慎考虑每个公开方法所申明的异常是很有必要的。因为一旦加了异常声明,在很长的一段时间内部都无法甩掉它。这也是为什么推荐使用非受检异常的一个重要原因,非受检异常不需要声明就可以直接抛出。但是对于一个方法抛出的非受检异常,也需要在文档中进行说明。