由浅入深Java的异常处理机制

由浅入深Java的异常处理机制

(一)概述

我们为什么要使用异常?首先我们可以明确一点就是异常的处理机制可以确保我们程序的健壮性,提高系统可用率。虽然我们不喜欢看到它,但是我们不能不承认它的地位和作用。

在没有异常机制的时候我们是这样处理的:通过函数的返回值来判断是否发生了异常(这个返回值通常是已经约定好了的),调用该函数的程序负责检查并且分析返回值。虽然可以解决异常问题,但是这样做存在几个缺陷:

  1. 容易混淆。如果约定返回值为-11111时表示出现异常,那么当程序最后的计算结果真的为-1111呢?
  2. 代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。
  3. 由调用函数来分析异常,这要求程序员对库函数有很深的了解。

在OO中提供的异常处理机制是提供代码健壮的强有力的方式。使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。下面我们就来了解一下Java中的异常。

Java中的异常被分为两大类:Runtime异常和非Runtime异常,其中非Runtime异常也被称作Checked异常。

  • Runtime异常:是Java程序运行时产生的异常,例如:数组下标越界、空指针异常、对象类型强制转换错误等。Error和RuntimeException都是Runtime异常,编译时对这类异常不做检查。
  • 非Runtime异常:也称Checked异常,例如IOException等。在编译时,编译器会对这类异常进行检查,看看有没有对这类异常进行处理,如果没有进行处理,编译会无法通过。

在这里插入图片描述

上图是异常类继承图,在Java中,任何异常都是Throwable类或其子类对象;Throwable有两个子类,分别是:Error和Exception。

  • Error:这是系统错误类,是程序运行时Java内部的错误,一般是由硬件或操作系统引起的,开发人员一般无法处理,这类异常发生时,只能关闭程序。
  • Exception:这是异常类,该类及其子类对象表示的错误一般是由算法考虑不周或编码时疏忽所致,需要开发人员处理。

(二)异常处理(try-catch-finally)

在这里插入图片描述

这里先介绍一下try-catch-finally执行流程最基本的情况,所谓最基本的情况就是没有用到throw关键字(抛出异常,后面会讲到)、return关键字等。

首先,执行try中的代码,如果 try中的代码没有发生异常,那么catch中的代码就不执行,等try中的代码执行完毕后直接执行finally中的代码。如果 try中的代码发生了异常(假设发生异常的代码语句是xxx),那么try中xxx下面的代码就不会执行了,会立即跳转到catch中去匹配异常,若匹配到了对应的catch中声明的异常对象,那么就执行该catch语句块中的代码,并且后面的catch语句块就不再执行。等catch语句块中的代码执行完毕,就执行finally语句块中的代码。此时,整个try-catch-finally才算执行完毕。

(三)声明抛出异常(throws)

当我们决定暂时不处理异常,而将其交给调用者去处理,我们就需要声明抛出异常。

声明抛出异常是一个子句,它只能写在方法头部的后面,其格式如下:throws <异常列表>。举例:public void function() throws IOException{…}。若在一个方法中声明抛出异常,那么调用该方法的调用者就必须对该异常进行处理,调用者处理该异常有两种方式:

  • 使用try-catch-finally来捕获并处理该异常。
  • 继续抛出,留给后面的调用者去处理,从而形成异常处理链。

(四)抛出异常

上面讲到的声明抛出异常只是告诉方法的调用者要去处理异常,这只是个说明性的语句,因为方法的代码中可能有异常,也可能没有异常(没有异常时就不会抛出)。而真正抛出异常的语句是throw <异常对象>,其中的异常对象必须是Throwable或其子类对象。例如:throw new Exception(“这是一个异常对象!”)。当执行到上述语句时,会立即结束方法的执行。

public void test1(){
    try{
        int a = 1 / 0; //发生异常的代码
        System.out.println("算术异常!!!");
    }
    catch(ArithmeticException | NumberFormatException e){
        System.out.println(e.getMessage()); //打印异常信息,并没有抛出异常
        throw new ArithmeticException("抛出算术异常!!!"); //抛出算术异常
    }
    finally{
        System.out.println("执行finally代码块!!");
    }
    System.out.println("已经抛出算术异常!!!");
}

try中的int a = 1 / 0;语句发生了算术异常(除数为0),直接跳转到catch中去匹配异常对象,匹配到了ArithmeticException异常对象,然后执行catch中的代码:打印异常信息,抛出异常。但是,在catch中抛出异常之前,会先执行finally中的代码,等finally中的代码执行完毕,再回到catch中抛出异常,而catch中的异常抛出后,整个方法就结束了,方法中剩下的代码就不执行了。
以上就是上述代码的完整执行流程。

(五)自定义异常类

如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。

按照国际惯例,自定义的异常应该总是包含如下的构造函数:一个无参构造函数 一个带有String参数的构造函数,并传递给父类的构造函数。 一个带有String参数和Throwable参数,并都传递给父类构造函数 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。 下面是IOException类的完整源代码,可以借鉴:

public class IOException extends Exception
{
    static final long serialVersionUID = 7818375828146090155L;

    public IOException()
    {
        super();
    }

    public IOException(String message)
    {
        super(message);
    }

    public IOException(String message, Throwable cause)
    {
        super(message, cause);
    }

    public IOException(Throwable cause)
    {
        super(cause);
    }
}

(六)异常的注意点

当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。例如,父类方法throws的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。

线程间的异常是独立的、互不影响的。Java程序可以是多线程的,每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

(七)异常的实践原则

1. 利用运行时异常设定方法使用规则

很常见的例子就是,某个方法的参数不能为空。在实践中,很多程序员的处理方式是,当传入的这个参数为空的时候,就返回一个特殊值(最常见的就是返回一个null,让用户方法决定怎么办)。还有的处理方式是,自己给一个默认值去兼容这种不合法参数,自己决定怎么办。这两种实践都是不好的。

对于第一种处理方式,返回值是用来处理正常流程的,如果用来处理异常流程,就会让用户方法的正常流程变复杂。一次调用可能不明显,当有多个连续调用就会变得很复杂了。对于第二种处理方式,看起来很强大,因为“容错”能力看起来很强,有些程序员甚至可能会为此沾沾自喜。但是它也一样让正常流程变复杂了,这不是最糟糕的,最糟糕的是,你不知道下一次用户会出什么鬼点子,传个你现有处理代码处理不了的东西进来。这样你又得加代码,继续变复杂……BUG就是这样产生的。

好的实践方式就是,设定方法的使用规则,遇到不合法的使用方式时,立刻抛出一个运行时异常。这样既不会让主流程代码变复杂,也不会制造不必要的BUG。为什么是运行时异常而不是检查异常呢?这是为了强迫用户修改代码或者改正使用方式——这属于用户的使用错误。

2. 消除运行时异常

当你的程序发生运行时异常,通常都是因为你使用别人的方法的方式不正确(如果设计这个异常的人设计错误,就另当别论。比如设计者捕获一个检查异常,然后在处理器抛出一个运行时异常给用户)。所以,发生运行时异常一般都是采取修改代码的方式,而不是新增一个异常流程。

3. 正确处理检查异常

处理检查异常的时候,处理器一定要做到下面的两个要求才算合格:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令。
  • 允许用户保存所有操作的结果,并以适当的方式终止程序。

对于检查异常,好的实践方式是:

  • 让可以处理这个异常的方法去处理,衡量的标准就是在你这个方法写一个处理器,这个处理器能不能做到开头的那两个要求,如果不能就往上抛。
  • 有必要的时候可以通过链式异常包装一下,再抛出。
  • 最终的处理器一定要做到开头的那两个要求。

2020年10月22日

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值