异常处理

这里写图片描述 异常处理

     **概述**
异常(Exception)是程序在执行过程中所产生的问题。导致异常的产生的原因有很多种,包括:用户输入了无效的数据、找不到一个需要打开的文件、在通讯过程中网络连接断开或者JVM发生了内存溢出等等。
有些异常是由于用户的错误所导致的,有些是由程序员的错误导致的,有些则是由硬件设备的故障导致的。在本章中,我们将详细介绍不同类型的异常,以及在什么时候应该抛出一个异常,在什么时候应该捕获一个异常,如何编写和抛出自定义的异常。
为了更好地认识和理解Java语言中异常处理的工作机制,我们首先需要认识异常的三个种类:
  检查异常:检查异常通常是用户错误或者不能被程序员所预见的问题。例如,如果要打开一个文件,但却无法找到该文件,此时就会产生异常。这种类型的异常被称为检查异常,它必须用Java语言来处理,而不能被简单的忽略。在后面介绍异常的处理和声明的规则时,我们将看到这种类型的异常。
  运行时异常:运行时异常是一个程序在运行过程中可能发生的、可以被程序员避免的异常类型。与检查异常不同的是,运行时异常可以被忽略。在程序开发时,我们应该让运行时异常使程序崩溃,然后找到问题所在,并更改代码,以使得异常不会再次发生。运行时异常的例子包括:数组越界、除数为零、引用为null、把引用类型转换为一个无效的数据类型等。
  错误:实际上,错误根本不是异常,但却是用户或程序员所无法控制的问题。错误通常在我们的代码中被忽略,虽然我们想在程序中来修复这个问题,但我们对一个错误却很少能有所作为。例如,如果发生调用栈溢出,将会导致一个错误。然而,由于内存不足,我们的程序将无法继续执行。我们所编写的任何程序代码都无法解决这一问题。因此,这样的错误通常在设计和编写Java应用程序时被忽略。

     **控制流程**
在Java语言中,异常(Exception)是被一个方法抛出的对象。当一个方法被调用时,这个方法被压入到内存的方法调用栈中。当一个方法抛出异常时,该方法从调用栈中被弹出,同时产生的异常对象抛给了栈中的前一个方法。例如,假如应用程序的main()方法在调用栈的底部,接着是method1()和method2()方法。如果method2()方法抛出一个异常,method2()方法就会从调用栈中被取出,同时异常抛给了下面的method1()方法。
对于这个异常的处理,method1()方法有三种选择:
  捕获这个异常,不让它沿着调用栈继续向下抛出;
  捕获这个异常,并继续向下抛出;
  不捕获这个异常,从而导致method1()方法从调用栈中被弹出,异常对象继续抛给调用栈下面的main()方法。
不论调用栈中有多少方法,程序控制流程都将继续在调用栈中向下执行。在调用栈中的每一个方法要么捕获并处理这个异常,要么捕获这个异常并再次抛出,或者什么都不做让异常进入下一个方法。
当这个异常到达调用栈的底部时会发生什么情况呢?如果Exception对象被抛给了main()方法,那么main()方法最好捕获该异常,否则程序就会终止。当一个异常到达调用栈的底部,如果没有方法来处理它,JVM将会崩溃,并且通知我们这个异常的详细情况。

    **throwable类**
Java语言中异常的继承层次关系就是基于这三个类。Error类是所有Java错误类的父类;Exception类是所有异常的父类,包括运行时异常和检查异常。
在这种继承层次关系中,运行时异常和检查异常根据适合的不同情况作出了进一步的区分。如果一个类是RuntimeException类的子类,那么这个子类代表了一个运行时异常。如果一个类是Exception的子类,但并不是RuntimeException的子类,那么这个类就是一个检查异常。
例如,ArrayIndexOutOfBoundsException和ArithmeticException是运行时异常,因为它们都是RuntimeException的子类。IOException和ClassNotFoundException是检查异常,因为它们都是Exception的子类。

这里写图片描述

Exception对象是Throwable类型的Java对象。当我们捕获到异常的时候,将获得一个指向Throwable类型对象的引用。每一个异常类都是不同的,并且拥有自己特定的方法。但是,由于所有的异常都继承于Throwable类,所以我们还可以在任何被捕获的异常上调用Throwable类的方法。
如下是Throwable类的一些方法的描述,更详细的方法描述请参考Java API文档:
  public String getMessage():返回关于已发生的异常的详细信息。这个信息在Throwable类的构造方法中被初始化。
  public Throwable getCause():返回Throwable对象所描述的异常产生的原因。这个原因由Throwable类的构造方法或initCause()方法进行初始化;
  public String toString():返回Throwable类的简短描述;                             public void printStackTrace():把toString()方法的结果与调用栈跟踪信息一起打印在控制台错误输出流中(System.err是Java程序运行在Windows窗口上的命令提示符)。为发送调用栈跟踪信息到指定输出流,该方法进行了重载。
  public StackTraceElement [] getStackTrace():返回一个包含调用栈跟踪信息每个元素的数组。索引号为0的元素表示调用栈的顶部,数组中的最后一个元素表示调用栈底部的方法。此方法允许我们应用程序以编程的方式遍历调用栈的每一行。
  public Throwable fillInStackTrace():使用当前调用栈跟踪信息来填充Throwable对象的调用栈跟踪信息,并添加以往任何信息到调用栈跟踪信息里。

      **捕获异常**
在Java语言中,我们通常在一个方法中使用try和catch关键字来捕获异常。使用try/catch关键字的代码块把可能产生异常的代码“包围起来”,其中的代码也被称为“被保护的代码”,使用try和catch的语法形式如下所示:
try {
//被保护的代码
} catch(异常的名称 e1) {
//捕获块
}

  多个catch块
  try{
  //被保护的代码
  }catch(ExceptionType1 e1){
  //Catch块
  }catch(ExceptionType2 e2){
  //Catch块
  }catch(ExceptionType3 e3){
  //Catch块
  }

      **异常处理及声明的规则**
在Java语言中对于检查异常有一个严格执行的规则,这个规则被称为异常处理和声明规则。这个规则指出一个检查异常要么被处理,要么被声明。处理异常是指异常的捕获,而声明异常是指一个方法在方法签名时使用throws关键字,但是声明的异常在该方法中不会被处理。
必须注意的是:异常处理和声明规则不适用于运行时异常。如果程序导致了运行时异常,我们可以选择捕获这个异常,或者干脆忽略它使得程序崩溃。如前所述,在多数情况下,不要试图去捕获运行时异常,因为它们往往都是由于糟糕的代码设计所造成的。解决的办法是让运行时异常使程序崩溃,然后发现问题并解决。
我们想强调运行时异常与检查异常之间的区别,并解释为什么运行时异常没有遵守异常处理和声明的规则。例如,使用点运算符来访问对象的方法或属性,如果对象引用是null,那就可能产生一个NullPointerException。而NullPointerException类是RuntimeException的子类,因此它也是一个运行时异常。如果每次在使用点运算符的时候都要试图去捕获NullPointerException的话,那么代码中将会包含很多的try/catch块。幸好我们可以在代码中忽略潜在的运行时异常。
但是,假如我们试图打开一个文件,但是这个文件却并不存在,如果我们简单忽略文件不存在的这个事实,那么程序的其它部分如何避免受到影响呢?在Java语言中,我们不能够忽略像文件没找到这种情形。我们必须处理潜在的检查异常,否则Java代码就不能通过编译。

    声明异常
如果一个方法没有处理检查异常,那么该方法必须使用throws关键字来声明异常。关键字throws出现在方法签名的末尾。例如,下面的方法就声明抛出一个RemoteException异常:
public void deposit(double amount) throws RemoteException
一个方法也可以声明它抛出多个异常。在这种情况下,多个异常之间用逗号进行分隔。例如,下面的方法声明它抛出一个RemoteException异常和一个InsufficientFundsException异常
public void withdraw(double amount)
throws RemoteException,InsufficientFundsException
在一个方法中我们到底什么时候处理异常,什么时候声明异常呢?这要取决于我们的设计决策:是想在方法里面处理问题,还是想把问题传递给方法的调用者。
如果问题是与方法相关的,那么我们应该让方法来处理自己的问题。例如,假设我们走进银行存款(通过调用deposit()方法),但是银行出纳员的计算机出问题了。这显然不是我们的问题。在这种情况下,deposit()方法必须在不通告方法调用者的情况下处理该异常,并修复故障。
为什么deposit()方法要抛出一个异常呢?假如我们在存款和银行计算机系统故障中间,这肯定是我们的问题。在这种情况下,我们应该被通知存款事务不能继续下去。deposit()方法可以通过向我们抛出一个异常,来告诉我们事务失败。
一个常见的程序设计方法是让方法返回一个布尔值。如果存款成功了,返回一个true,否则返回一个false。那么,我们在这里为什么不采用这种方法呢?这是因为它并没有告诉我们到底出了什么错。假设我们去存钱,结果银行出纳员只是说“对不起,存不了”,那么我们就不知道为什么存不了。如果银行出纳员向我们抛出一个异常,那么我们就可以捕获该异常(这个异常是个Java对象),并判断到底是哪里出错了。
抛出一个异常看起来需要太多的开销。那么我们值得这样做么?异常处理是Java的一部分,与设计带来的好处相比,最小的开销不必关心。例如,如果我们试图取出超过存款账户余额的钱,只是用一个false返回值来告诉我取钱事务失败,并不能阻止我们透支。这样我们可能花了更多的钱,然后在透支通知函来的时候感到莫名其妙。我们可以告诉银行,我们不能检查withdraw()方法的返回值。
那么为什么在这种情况下,异常是一个更好的设计呢?这是因为如果一个异常是检查异常,我们就必须处理该异常。所以,如果withdraw()方法抛出了一个InsufficientFundsException(余额不足)异常,那么我们就必须在每次调用withdraw()方法时处理该异常。当然,一旦我们捕获了该异常,我们也做不了什么,我们还是会一直花我们没有的钱,但是我们现在不会再以不知道情况为借口,对银行说不知道透支了。与接口可以用于强制类的行为相似,异常处理或声明规则可以用于强制方法的调用者来处理潜在的问题,而不是简单地忽略它们。

throws关键字
我们可以通过使用throw关键字来抛出异常,这个异常可以是一个新的异常实例,也可以是一个我们刚刚捕获的异常。throw语句将导致当前代码立即停止执行,而且异常将被抛给调用栈中的前一个方法。

    **finally关键字**
关键字finally用于在try块后创建一个代码块。finally代码块总是会执行,不管异常是否发生。我们可以使用finally块来执行清理类型的语句,而不管被保护的代码中发生了什么。finally块出现在catch块的末尾,语法形式如下:
try {
//被保护的代码
}catch(ExceptionType1 e1) {
//Catch块
}catch(ExceptionType2 e2) {
//Catch块
}catch(ExceptionType3 e3) {
//Catch块
}finally {
//finally块总会执行。
}
为什么要用finally块呢?假如我们对于一个已经打开的文件,不管该文件能够成功读取数据,还是因为某种原因读取数据失败,我们都要关闭该文件。这时候,用finally块就可以简化代码。

      **用户自定义异常**
在Java中,我们可以创建自定义异常。实际上,因为Java被设计的方式,我们被鼓励来编写自定义的异常,以代表在我们的类中会出现的问题。在编写自定义异常类时,必须牢记如下几点:
所有的异常必须是Throwable的子类;
如果我们想编写一个可以自动被异常处理或声明规则强制的检查异常,就需要继承Exception类;
如果想编写一个运行时异常,就需要继承RuntimeException类。
我们肯定不会编写一个直接继承Throwable类的类,因为此后这个类既不是检查异常,也不是运行时异常。大多数用户自定义的异常类都被设计为检查异常,因而会继承Exception类。但是,如果我们想编写一个不想让用户处理或声明的异常,就应该通过继承RuntimeException类来使它成为一个运行时异常。


注:
异常的类型:
1、  检查异常,也被称之为“编译时异常”,由编译器检查出来的异常
2、  运行时异常,编译器无法检查出来,只有程序在运行时,抛出的异常,才是运行时异常
3、  错误,错误实际上已经脱离了“异常”的范畴,因为程序员无法通过修改代码来解决问题了,比如:内存不足,硬盘不足等

对于一个异常的处理,在Java中,有三种方式:
1、  方法本身直接抓捕异常,不让这个“异常”对象沿着方法调用栈继续向下传递
2、  方法本身直接抓捕异常,并继续让这个“异常”对象沿着方法调用栈继续向下传递
3、  不去抓捕异常,就让这个“异常”对象沿着方法调用栈继续向下传递,并且让方法从方法栈中直接弹出

编译时异常的解决方案:
要么直接异常处理,要么就给方法的调用者进行“异常声明”
而运行时异常的解决方案,不需要异常处理,也不需要“异常声明”,让它报错,报错以后,我们程序员再通过“条件控制语句”对其进行控制即可

throws关键字,用来给方法的调用者,提前声明:我这个方法可能会抛出异常
如果在代码中,需要抛出某个异常,请使用throws关键字
方法异常的声明,通常用于编译时异常,而运行时异常不需要

当一个方法中,可能抛出不计数的异常个数,解决方案两种:
1、  定义多个catch块,分别进行代码的监控
2、  在catch块的参数类型定义时,直接定义上层父类,采用动态多态的方式来解决问题
另外一个需要注意的是:
在定义多个catch块的时候,定义的顺序需要遵循从小到大原则

事务:具有明确边界的
参与到同一个事务范围内的业务方法,为了保证事务的完整性,他们都不能自己抓取异常,只能声明异常,然后由外部进行事务控制。

当方法的调用者,需要知道“调用方法”的具体执行情况时,而并非是成没成功,失没失败时,我们就有必要针对“调用方法”进行异常声明,比如:transfer() 想知道“张三给李四转钱5000”这个过程中,所有的执行方法的具体执行情况时。
在方法异常声明的时候,如果遇到子异常和父异常,都可能抛出的情况下,建议声明父异常,因为子异常可以向父异常自动向上转型
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值