微信图片_20190903161819.jpg

上面是Java异常类的组织结构,红色区域的异常类表示是程序需要显示捕捉或者抛出的。

异常的分类

Throwable

Throwable是Java异常的顶级类,所有的异常都继承于这个类。

Error,Exception是异常类的两个大分类。

Error

Error是非程序异常,即程序不能捕获的异常,描述了Java运行时系统内部错误和资源耗尽错误。应用程序不应该抛出这类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了。这种情况很少出现。

Exception

Exception是程序异常类,由程序内部产生。Exception又分解为两个分支,一个分支派生于RuntimeException,即运行时异常,另一个分支为非运行时异常。由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像IO错误这类问题导致的异常属于其他异常。

受检异常

在编译时需要检查的异常,需要用try-catch或throws处理。在Java中主要指除了ErrorRuntimeException之外的异常。

非受检异常

不需要在编译时处理的异常。在java中派生于ErrorRuntimeException的异常都是unchecked exception,其他都是checked exception.

受检异常和非受检异常的区别

  1. public class Test {
  2. public void doSomething()throws ArithmeticException{
  3. System.out.println("I am in");
  4. }
  5. public static void main(String[] args) {
  6. Test test = new Test();
  7. test.doSomething();
  8. }
  9. }

问题1:上面的程序能否编译通过?并说明理由。
解答:能编译通过。分析:按照一般常理,定义doSomething方法是定义了ArithmeticException异常,在main方法里 里面调用了该方法。那么应当继续抛出或者捕获一下。但是ArithmeticException异常是继承RuntimeException运行时异常。

对于非受检异常,java编译器不要求你一定要把它捕获或者一定要继续抛出。因为对于非受检异常,要么不可控制(Error),要么就应该避免发生(RuntimeException)。

问题2:上面的程序将ArithmeticException改为IOException能否编译通过?并说明理由。
解答:不能编译通过。分析:IOException extends Exception 是属于checked exception,必须进行处理,或者必须捕获或者必须抛出。

对非受检异常(unchecked exception)的几种处理方式:
1、捕获
2、继续抛出
3、不处理
对受检异常的几种处理方式:
1、继续抛出,消极的方法,一直可以抛到java虚拟机来处理
2、用try…catch捕获
注意,对于检查的异常必须处理,或者必须捕获或者必须抛出

警告:如果在子类中覆盖了父类的一个方法,子类方法中声明的受检异常不能比父类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常。)如果父类方法没有抛出任何受检异常,子类也不能抛出任何受检异常。

自定义异常

Q:为什么要自定义异常呢?

A:在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。在这种情况下,创建自己的异常类就可以充分解释清楚问题。我们需要做的只是定义一个派生于Exception的类,或者派生于Exception子类的类。

自定义异常类应该包括两个构造器,一个是默认构造器,一个是带有详细描述信息的构造器。(父类Throwable的toString方法将会打印出这些详细信息,这在调试中非常有用)。

  1. public class MyException extends Exception {
  2. public MyException() {
  3. }
  4. public MyException(String message) {
  5. super(message);
  6. }
  7. }

处理异常

如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。

捕获异常

对于程序中出现的异常,对于非受检异常我们可以选择继续抛出,也可以选择捕获进行处理,对于受检异常我们也可以捕获。下面是一些捕获异常的方法

try/catch语句块

利用try/catch语句块将可能抛出异常的代码包起来,再利用catch捕获可能出现的异常类型,便可以在catch里进行处理。这里比较简单不再多说。

  1. public void read(String fileName){
  2. try{
  3. InputStream in = new FileInputStream(fileName);
  4. int b;
  5. while((b = in.read()) != -1){
  6. //...
  7. }
  8. }catch(IOException exception){
  9. //处理
  10. }
  11. }
  12. //另外之前提过的,对于非受检异常,我们可以什么都不做而继续抛出。而对于受检异常我们如果不处理也可以继续抛出,让异常方法调用者去处理
  13. public void read(String fileName) throws IOException{
  14. InputStream in = new FileInputStream(fileName);
  15. int b;
  16. while((b = in.read()) != -1){
  17. //...
  18. }
  19. }

如果想传递一个异常,必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。

仔细阅读一下Java API文档,以便知道每个方法可能会抛出哪种异常,然后再决定是自己处理还是添加到throws列表中

捕获多个异常

  1. public void test(){
  2. try{
  3. //test code
  4. }catch(FileNotFoundException e){
  5. //exception 1
  6. }catch(UnKnownHostException e){
  7. //exception 2
  8. }catch(IOException e){
  9. //exception 3
  10. }
  11. }
  12. //另外可以合并catch字句
  13. //注意!!!只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性
  14. public void test(){
  15. try{
  16. //test code
  17. }catch(FileNotFoundException | UnKnownHostException e){
  18. //exception 1 2
  19. }catch(IOException e){
  20. //exception 3
  21. }
  22. }

捕获多个异常不仅会让你的代码看起来更简单,还会更高效。生产的字节码只包含一个对应公共catch子句的代码块。

再次抛出异常

在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么用于表示系统故障的异常类型可能会产生多种解释。例如ServletException,执行servlet的代码可能不想知道发生错误的细节原因,但希望明确地知道servlet是否有问题。

  1. //捕获异常并将它再次抛出
  2. public void test(){
  3. try{
  4. //database
  5. }catch(SQLException e){
  6. throw new ServletException("database error" + e.getMessage());
  7. }
  8. }

下面有一种更好的处理方法,并将原始异常设置为新异常的“原因”

  1. public void test(){
  2. try{
  3. //database
  4. }catch(SQLException e){
  5. Throwable se = new ServletException("database error");
  6. se.initCause(e);
  7. throw se;
  8. }
  9. }
  10. //当捕获到异常时,就可以使用下面这条语句重新获得原始异常
  11. Throwable e = se.getCause();

在书中作者强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

finally子句

当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。但是如果方法需要回收释放一些资源,但是只有这个方法自己知道,这时就可以用到finally子句。

不管是否有异常捕获,也不管try块里是否有return并且是否执行了,finally子句里的代码始终都会在最后执行。比较简单也不过多解释。

  1. try{
  2. //code
  3. }catch(Exception e){
  4. //handle
  5. }finally{
  6. //final handle
  7. }
  8. //有一个返回值覆盖的问题,也是上面提到的return
  9. try{
  10. //...
  11. return 0;
  12. }catch(Exception e){
  13. //...
  14. }finally{
  15. return 1;
  16. //最终返回1
  17. }
  18. //在这段代码里,如果没有发生异常,try块里会执行return,但在真正返回之前,会执行finally里的代码,这时返回值就会被覆盖为1。

try-with-resources

对于try/finally子句,我们可以在finally子句里进行资源回收释放操作,但我们也可以使用另一种方法进行资源的关闭,那就是带资源的try语句。

假设资源属于一个实现了AutoCloseable接口的类,该接口下有close方法,Java SE 7 就提供了一个很有用的快捷方式来自动关闭资源。

  1. try(Resource res = ...){
  2. //...
  3. }

在try块退出时,会自动调用res.close方法。

  1. try(Scanner in = new Scanner(new FileInputStream("/user")),"UTF-8"){
  2. while(in.hasNext()){
  3. System.out.println(in.next());
  4. }
  5. }

在这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用finally块一样。只要需要关闭资源,就要尽可能使用带资源的try语句。

使用异常机制的技巧

  • 异常处理不能代替简单的测试

在程序中,不要使用抛出异常的方法来测试一段代码。简单的条件判断和捕获异常的方法所花费的时间后者大大超过了前者,因此使用异常的基本规则是:只在异常情况下使用异常机制。

  • 不要过分细化异常

主要是不要把一段代码用不同的try-catch块分装,这种编程方式会导致代码里的膨胀,降低可读性。因此有必要将整个任务包装在一个try语句块中。

  • 利用异常层次结构

不要只抛出RuntimeException异常,应该寻找更加适合的子类或创建自己的异常类。

不要只捕获Throwable异常,否则,会使程序代码更难读,更难维护。

  • 不要压制异常

在Java中,往往强烈地倾向关闭异常。如果一个方法调用了另一个可能出现异常的方法,但是这个方法有可能很久很久才会抛出一个异常,但是编译器会因为你没有把该异常列在throw里不予通过。此时如果你将这个异常列在throws里以后所有调用这个方法的方法都要考虑处理这个异常。因此应该将这个异常关闭。

  1. try{
  2. //code
  3. }catch(Exception e){}
  4. //啥也不做,关闭异常
  • 在检测错误时,“苛刻”要比放任更好

在用无效的参数调用一个方法时,返回一个虚拟的数值,还是抛出一个异常,哪种方法更好?例如,当栈为空时,Stack.pop是返回一个null,还是抛出一个异常?在核心卷里认为,在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException异常更好。

  • 不要羞于传递异常

很多人感觉应该捕获抛出的全部异常。但是例如FileInputStream异常,与其捕获该异常,传递异常会更好。这样可以让更高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。

第五个和第六个规则可以归纳为 “早抛出,晚捕获

使用断言

概念

假设确信某个属性符合要求,并且代码的执行依赖于这个属性。

断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检测语句会被自动地移走。

使用断言

在Java里定义了一个关键字 assert,这个关键字有两种使用形式:

  1. assert 条件;
  2. assert 条件 : 表达式;

这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常,在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。

“表达式”部分的唯一目的时产生一个消息字符串。AssertionError对象并不存储表达式的值,因此,不可能在以后得到它。

启用和禁用断言

在默认情况下,断言被禁用。可以在运行程序时用 -enableassertions-ea 选项启用。

java -enableassertions MyApp

在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器(class loader)的功能。

java -ea:MyClass -ea:com.topview.mylib... MyApp

这条命令将开启MyClass类以及在com.topview.mylib包和它的子包中所有的类的断言。

也可以用-disableassertions-da禁用断言

java -ea:... -da:MyClass MyApp

这条命令用于禁用某个特定的类和包的断言。

另外,启用和禁用所有断言的-ea-da开关不能应用到那些没有类加载器的“系统类”上。对于系统类来说,需要使用-enablesystemassertions/-esa开关启用断言。

另外,idea启用断言的方法如下:

image.png

注意:断言只应该用于在测试阶段确定程序内部的错误位置

日志

优点

在Java中有自带的日志记录,利用记录日志API可以有以下优点

  • 可以很容易地取消全部日志记录,或者仅仅取消某个级别地日志,而且打开和关闭这个操作也很容易
  • 可以很简单地禁止日志记录地输出
  • 日志记录可以被定向到不同地处理器,用于在控制台中显示,用于存储在文件中等
  • 日志记录器和处理器都可以对记录进行过滤
  • 日志记录可以采用不同地方式格式化
  • 应用程序可以使用多个日志记录器
  • 在默认情况下,日志系统地配置由配置文件控制。如果有需要可以替换这个配置。

七个级别

日志记录器有以下7个级别

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

在默认情况下,只记录前三个级别

对于日志不过多描述。目前使用普遍的Log4j,Slf4j等日志框架,可以深入了解学习这些日志系统。