简析java中的异常处理机制

 

  • Java中的异常类

在计算机程序运行的过程中,总是会出现各种各样的错误。

有一些错误是用户造成的,比如,希望用户输入一个int类型的年龄,但是用户的输入是abc,又或者程序想要读写某个文件的内容,但是用户已经把它删除了,等等。这些错误是可以避免的。

还有一些错误是随机出现,并且永远不可能避免的。比如:

  • 网络突然断了,连接不到远程服务器;
  • 内存耗尽,程序崩溃了;
  • 用户点“打印”,但根本没有打印机;
  • ……

所以,一个健壮的程序必须处理各种各样的错误。

所谓错误,就是程序调用某个函数的时候,如果失败了,就表示出错。

有两种方法来进行错误的信息处理,一是约定错误返回码,用int类型表示错误类型,常见于底层c函数。第二是在语言层面上提供异常处理的机制。

Java内置了一套异常处理机制,总是使用异常来表示错误。

异常是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了,Java的异常是class,它的继承关系如下:

从继承关系可知:Throwable是异常体系的根,它继承自Object。Throwable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

而Exception则是运行时的错误,它可以被捕获并处理。

某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:

  • NullPointerException:对某个null的对象调用方法或字段
  • IndexOutOfBoundsException:数组索引越界

Exception又分为两大类:

  1. RuntimeException以及它的子类;
  2. 非RuntimeException(包括IOException、ReflectiveOperationException等等)

Java规定:

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。

 

  • 捕获异常

在Java中,凡是可能抛出异常的语句,都可以用try ... catch捕获。把可能发生异常的语句放在try { ... }中,然后使用catch捕获对应的Exception及其子类。

可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

简单地说就是:多个catch语句只有一个能被执行。例如:

public static void main(String[] args) {

    try {

        process1();

        process2();

        process3();

    } catch (IOException e) {

        System.out.println(e);

    } catch (NumberFormatException e) {

        System.out.println(e);

    }

}

存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:

public static void main(String[] args) {

    try {

        process1();

        process2();

        process3();

    } catch (IOException e) {

        System.out.println("IO error");

    } catch (UnsupportedEncodingException e) { // 永远捕获不到

        System.out.println("Bad encoding");

    }

}

对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行。

因此,正确的写法是把子类放到前面:

public static void main(String[] args) {

    try {

        process1();

        process2();

        process3();

    } catch (UnsupportedEncodingException e) {

        System.out.println("Bad encoding");

    } catch (IOException e) {

        System.out.println("IO error");

    }

}

无论是否有异常发生,如果我们都希望执行一些语句,例如清理工作,怎么写?

可以把执行语句写若干遍:正常执行的放到try中,每个catch再写一遍。例如:

public static void main(String[] args) {

    try {

        process1();

        process2();

        process3();

        System.out.println("END");

    } catch (UnsupportedEncodingException e) {

        System.out.println("Bad encoding");

        System.out.println("END");

    } catch (IOException e) {

        System.out.println("IO error");

        System.out.println("END");

    }

}

上述代码无论是否发生异常,都会执行System.out.println("END");这条语句。

那么如何消除这些重复的代码?Java的try ... catch机制还提供了finally语句,finally语句块保证有无错误都会执行。上述代码可以改写如下:

public static void main(String[] args) {

    try {

        process1();

        process2();

        process3();

    } catch (UnsupportedEncodingException e) {

        System.out.println("Bad encoding");

    } catch (IOException e) {

        System.out.println("IO error");

    } finally {

        System.out.println("END");

    }

}

注意finally有几个特点:

  1. finally语句不是必须的,可写可不写;
  2. finally总是最后执行。

如果没有发生异常,就正常执行try { ... }语句块,然后执行finally。如果发生了异常,就中断执行try { ... }语句块,然后跳转执行匹配的catch语句块,最后执行finally。

可见,finally是用来保证一些代码必须执行的。

通过printStackTrace()可以打印出方法的调用栈,类似:

java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:614)
    at java.base/java.lang.Integer.parseInt(Integer.java:770)
    at Main.process2(Main.java:16)
    at Main.process1(Main.java:12)
    at Main.main(Main.java:5)

printStackTrace()对于调试错误非常有用。

 

  • 抛出异常

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try ... catch被捕获为止。

当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。

如何抛出异常?抛出异常分两步:

  1. 创建某个Exception的实例;
  2. 用throw语句抛出。

下面是一个例子:

void process2(String s) {

    if (s==null) {

        throw new NullPointerException();

    }

}

如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:

void process1(String s) {

    try {

        process2();

    } catch (NullPointerException e) {

        throw new IllegalArgumentException();

    }

}




void process2(String s) {

    if (s==null) {

        throw new NullPointerException();

    }

}

当process2()抛出NullPointerException后,被process1()捕获,然后抛出IllegalArgumentException()。

如果在main()中捕获IllegalArgumentException,打印的异常栈类似于:

java.lang.IllegalArgumentException
    at Main.process1(Main.java:15)
    at Main.main(Main.java:5)

这说明新的异常丢失了原始异常信息,我们已经看不到原始异常NullPointerException的信息了。

为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息。对上述代码改进如下:

    static void process1() {

        try {

            process2();

        } catch (NullPointerException e) {

            throw new IllegalArgumentException(e);

        }

}

运行上述代码,打印出的异常栈类似:

java.lang.IllegalArgumentException: java.lang.NullPointerException
    at Main.process1(Main.java:15)
    at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
    at Main.process2(Main.java:20)
    at Main.process1(Main.java:13)

注意到Caused by: Xxx,说明捕获的IllegalArgumentException并不是造成问题的根源,根源在于NullPointerException,是在Main.process2()方法抛出的。

在代码中获取原始异常可以使用Throwable.getCause()方法。如果返回null,说明已经是“根异常”了。

有了完整的异常栈的信息,我们才能快速定位并修复代码的问题。

posted @ 2022-06-07 15:13  WildMice  阅读(301)  评论(0编辑  收藏  举报