1 为什么要处理异常?
- 异常机制可以使程序的异常处理代码与正常业务代码分离,保证程序代码的健壮性。
- 在设计程序的时候,好的程序需要尽可能处理已知的可能产生的错误,但是事实上并不可能考虑到所有可能产生异常的情况,同时,众多的类似if-else的错误处理代码可能会带来很多的代码冗杂,错误处理与业务代码混杂的情况,带来阅读和维护的难度。因此我们需要合适的异常处理机制。
- java的异常机制依靠于try,catch,finally,throw,throws关键字,其中try块中通常放置可能引发异常的代码,catch后对应异常类型和响应的异常处理代码块,finally块在java异常机制中总是会被执行,通常用于回收try块中打开的物理资源,throw用于抛出一个实际的异常(异常的实例),throws主要在方法签名中使用,用于声明该方法可能会抛出的异常,方便或者提醒方法的使用者来捕获并处理异常。
2 异常处理机制
2.1 异常与异常类的继承体系
- 在java程序中,当程序出现意外情况时,系统会自动生成一个Exception来通知程序,从异常发生出逐渐向外传播,如果没有人来处理该异常,就会交给jvm来处理,jvm对异常的处理方法是,打印异常跟踪栈信息,并中止程序的执行。
- java提供了丰富的异常类体系,所有的异常类都继承自Throwable父类
- 注意:
- Error错误:一般与虚拟机相关,如系统崩溃,动态链接错误等,这种错误无法恢复也无法捕获,所以应用程序一般不能处理这些错误,也不应该试图去捕获这类对象
- 我们主要处理的Exception类,该类在java中分为两类,一种是Checked异常,一种是Runtime异常。Checked异常是java特有的,在java设计哲学中,Checked异常被认为是可以被处理或者修复的异常,所以java程序必须显式处理Checked异常,当我们使用或者出现Checked异常类的时候,程序中要么显式try- catch捕获该异常并修复,要么显式声明抛出该异常,否则程序无法通过编译。(Checked异常某种程度上降低了开发生产率和代码执行率,在java领域是一个备受争论的问题)
- 常见的运行时异常:如上图所示,这里列举几个比较常见的。
public class test{
public static void main(String[] args){
try{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1));
int c = a/b;
System.out.println("您输出的结果是"+c);
}
catch(IndexOutOfBoundsException ie){
System.out.println("数组越界,输入的参数不够");
}
catch(NumberFormatException ne){
System.out.println("数字格式异常:程序只能接收整数参数");
}
catch(ArithmeticException ae){
System.out.println("算术法异常");
}
catch(Exception e){
System.out.println("出现异常");
}
Date d = null;
try{
System.out.println(d.after(new Date()));
}
// 试图调用一个null对象的方法或者实例变量时 出现的异常
catch(NullPointerException ne){
System.out.println("指向异常");
}
}
}
复制代码
- 程序中一般将Exception放在最后,先捕获小异常(子类异常),再捕获大异常。如果顺序颠倒,还会出现编译错误。
2.2 try ... catch异常捕获
try ... catch 是最常见的异常捕获语句,try后与{ }配对使用,其中是业务实现代码,如果try块中出现问题或者异常,系统自动生成一个异常对象,该异常对象提交到Java运行环境,java运行环境收到异常对象后,会寻找能够处理该异常的catch块,如果找到,就将异常对象交给该catch块处理,如果没有找到,就终止程序运行。
从异常捕获过程中,我们可以看到程序要慎重考虑可能出现异常的代码块,否则这段程序是不够健壮的,一个经常出现崩溃或被终止的程序,是灾难性的。
-
catch块中如何处理异常:
- 一个try块之后可能存在多个catch块,java运行时与catch块()内的异常类进行比较,判断该异常是否 instanceof 该类,如果属于该类,就将该异常对象传给catch块内的异常形参,catch块后可以对该异常进行处理,获取相关异常的详细信息等。注意系统生成的异常实例对象是相对具体的子类异常对象,而进入一个catch块后就不会再进入下一个catch块,所以这也是我们尽量将小异常放在大异常前面的原因。
- 常见操作:输出出现异常提示信息
- 访问异常信息,打印异常信息
getMessage();// 返回详细描述字符串 printStackTrace();// 打印异常跟踪栈信息到标准错误窗口 printStackTrace(PrintStream s);// 跟踪栈信息输出到指定输出流 getStackTrace();// 返回跟踪栈信息 复制代码
- 采用别的替选数据或方案,或者提示用户重新操作,或者重新抛出异常,进行异常转译,重新包装,交给上层调用者来对该异常进行处理。
多异常捕获:(java 7 提供的新特性)
public class test{
public static void main(String[] args){
try{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1));
int c = a/b;
System.out.println("您输出的结果是"+c);
}
catch(IndexOutOfBoundsException ie | NumberFormatException ne
|ArithmeticException ae){
System.out.println("程序发生上述异常的某一种");
// 此时ie ,ne, ae都是默认 final修饰的变量 不能再重新赋值
}
catch(Exception e){
System.out.println("出现异常");
// 此时可以赋值
e = new RuntimeException("new error");
}
}
}
复制代码
2.3 finally 一定会执行的finally模块
- 通常try块里打开了一些物理资源,(比如磁盘文件,数据库连接等),这些需要保证回收,所以我们通常在finally块中进行回收。
- 举个例子
public class test{
public static void main(String[] args){
FileInputStream fis = null;
try{
fis = new FileInputStream("a.txt");
}
catch(IOException e){
System.out.println(e.getMessage);
//如果执行return,程序会先跳到finally块执行,执行完之后再回来执行return语句
return;
// 如果这里是System.exit(0),因为是退出虚拟机
// 所以finally块没办法执行
}
finally{
if(fis!= null){
try{
fis.close();
}
catch(IOException e){
e.printStackTrace();
}
}
}
}
}
复制代码
- 注意:尽量不要在finally块中使用return或者throw语句,因为一旦在finally中执行,程序就不会再跳回原来try或者catch块中执行原本应该执行的return和throw语句了,程序自己就结束了,这可能会带来一些麻烦的错误。
- 为了方便物理资源的关闭,java7 提供了一种新的语法,增强了try的功能,可以在try后面跟一对圆括号,来声明初始化物理资源,try执行完毕后会自动关闭资源。
public class AutoClose{
public static void main(String[] args)
throws IOException
{
// ()内的资源类必须实现AutoCloseable 或者Closeable接口中的close()方法
try(BufferedReader br = new BufferedReader(
new FileReader("auto.java")))
{
System.out.println(br.readLine());
}
}
}
复制代码
2.4 throws 关键字:声明抛出异常
throws声明抛出异常,在方法签名中使用,上面的AtuoClose就是其使用的例子。它可以声明抛出多个类,多个类之间用“,”隔开。
首先要理解我们为什么要声明抛出异常:当某个方法中程序的执行可能会出现异常,但是该方法并不知道如何处理异常,或者我们想把这个异常交给上层方法调用者来处理或者修复,那我们给该方法加上关键字throws 异常,以声明该方法可能会出现的异常。自然,加了throws关键字之后,该方法我们就无需再用try—catch来捕获异常了,因为这已经不是我们这个方法需要操心的事情了。
注意使用throws声明异常的时候,涉及到子类对父类的方法重写时,子类声明的异常类型应该是父类方法声明的异常类型的子类或者相同类。
如果throws 声明的是checked异常,根据checked异常的规定,我们不能对该异常视而不见,因为我们必须处理该异常,所以当拿到一个声明了可能会发生checked异常的方法时,在调用该方法时,要么放在try块中来显式捕捉该异常,要么放在另外一个带throws声明异常的方法中。
所以使用checked异常时,要特别注意处理它的问题,还会带来方法重写的限制性,因此大部分时候推荐使用Runtime异常。
public class ThrowTest{
public static void main(String[] args)
throws Exception
{
test();//test 声明会产生checked 异常 因此main函数也需要声明异常
// 或者在try - catch 中捕获该异常
}
public void test() throws IOException{
FileInputStream fis = new FileInputStream("a.text");
}
}
复制代码
2.5 throw关键字 :抛出异常
java允许程序自行抛出异常,通常系统帮助我们检查是否发生一些普遍定义的异常,但是有些异常可能不是普遍定义的,只是与我们业务不符,所以我们可以自行抛出异常,也可以自行抛出一些我们自定义的异常,而抛出异常的行为与系统抛出异常的行为一定程度上是等价的,后续处理方式也是一样的,在这里我们使用throw关键字。
throw语句可以单独使用,注意它抛出的是一个异常实例。
try{
// do something...
throw new Exception("hhh 我是新异常"); // 异常实例
}
catch{
System.out.println("出现异常");
continue;
}
复制代码
当我们自行抛出的异常是checked异常的时候,该throw语句要么是在如上面例子中的try块中,显示捕获,要么是在一个已经用throws声明会出现异常的方法中,而如果我们抛出的是runtime异常,那么情况就很简单了,它无需在try块中,也不需要将对应的方法用throws声明,如果我们想要处理,就捕获处理它,不管是在它自身方法体内,或者是对应方法者,也可以不去理会,当然我们自己抛出的异常,通常情况下是要处理的,不然抛出去之后不管最后只能中断程序运行了,只不过抛出是runtime异常时,在编译时没有那么严格。
-
自定义异常类:前面说了,系统会抛出一些普遍意义的异常,那么我们也就没必要再自己操心throw了,通常throw的是自定义的异常类。
- 自定义异常类都应该继承Exception类,或者Exception下的子类如runtime异常
- 定义异常类的时候需要提供两个构造器,一个无参构造器,一个带一个字符串参数的构造器,这串字符串实际上是getMessage() 时返回的异常对象的详细描述信息。
public class myException extends Exception{
public myException(){};
public myException(String msg){
super(msg);
}
}
复制代码
- catch中throw(抛出)异常:有时候我们在本方法中捕捉了异常,我们只能处理异常的一部分,我们还需要别的方法来处理或者我们想把产生了异常的这个信息告诉调用者,这个时候我们通常捕捉了异常后会在catch块中抛出我们想抛出的异常
- 在企业级应用中,通常对异常处理分为两部分:应用后台打印或者通过日志记录异常发生时详细情况(异常跟踪栈)和向使用者传达某种提示。
- java 7 中增强了throw语句:java 7 编译器执行更细致检查,检查抛出的异常的具体类型。
public class Test{
private double initprice = 30.0;
// 自定义的异常
// 方法中抛出了异常 所以此处要声明异常
public void bid(String bidprice) throws AuctionException{
double d =0.0;
try{
d = Double.parseDouble(bidprice);
}
catch(Exception e){
e.printStackTrace();
throw new AuctionException("新异常");
//这里也可以抛出e 但是我们通常会抛出我们包装过后的异常
// 用来向方法调用者或者上层提示某种信息 而不会直接暴露异常的原因
// throw e;
// 如果这里throw e 在java7 以前不会做细致检查,throws声明那里必须声明为Exception
// 但是java7 之后可以声明为更细致的异常子类型
}
}
public static void main(String[] args){
Test test = new Test();
try{
test.bid("df");
}
catch(AuctionException ae){
System.err.println(ae.getMessage);
}
}
}
复制代码
- 异常链:异常转译与异常链
3. 异常处理的一些基本规则
3.1 异常跟踪栈
- 异常是从里向外传播,可以很方便跟踪异常的发生情况,可以用来调试程序,但在发布的程序中,应该避免使用,而是将异常进行适当的处理。
3.2 不要过度使用
- 异常的运行效率会差一些,因此如果不是那种不可预期的错误,应该避免使用异常,而是放在正常的业务判断或者处理逻辑中。
3.3 不要使用过于庞大的try块
- 为了更好的判断发生的异常的类型,应该将大块try块分割为多个可能出现异常的try段落。
3.4 避免catch-all
- 类似上面的理由,更好的区分度,更好的异常判断
3.5 不要忽略异常
- 避免catch块为空,或者仅仅打印出异常情况,我们还是要处理异常,比如绕过异常发生的地方,或者采用别的替选数据或方案,或者提示用户重新操作,或者重新抛出异常,进行异常转译,重新包装,交给上层调用者来对该异常进行处理。