聊聊Java中的异常(基础篇)

629 阅读8分钟

《Java编程思想》里面有一句话:Java的基本理念是“结构不佳的代码是不能运行的”。个人觉得,这可以从两个层面来理解,一是代码自身问题,有错误(在编译时期或者运行时期出现错误)的代码是不能继续运行下去的。二是开发者对代码质量的极致要求,我们决不允许有影响系统的正常运行的代码存在。

对于编译时期出现错误的代码我们可以及时发现修正,但是对于运行期出现异常的代码我们是很难提前发现并修正的。这就需要我们学会捕获异常并处理异常,保证系统的稳定运行。

什么是异常

在《Java编程思想》里面就说的很清楚了:

异常情形是指阻止当前方法或者作用域继续执行的问题。把异常问题和普通问题相区分很重要,所谓普通问题是指,在当前环境下能够得到足够的信息,总能处理这个问题。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前的环境跳出,并把问题交给上一级。这就是抛出异常所发生的事情。

简单来说异常就是阻止程序继续运行下去,这时候JVM就会抛出异常。

在JVM抛出异常时,会在堆上创建异常对象,然后当前的执行路径被终止,并且从当前环境中弹出刚才创建的异常对象的引用。接着,异常处理机制接管程序。如果有catch、finally语句就会在catch语句处理异常并执行finally语句。当然,可以没有finally语句,但是如果需要处理异常需要在catch语句里面执行。虽然finlly语句最终也会被执行,但是建议在catch语句处理异常,同时catch接受异常对象作为参数,方便异常的处理。

异常的分类

Java中的异常大致可以分为两类:编译时期异常和运行时异常。编译时期异常就是在代码编译阶段可以发现的异常,比如使用javac命令编译代码,如果代码有错误就会编译不过并提示错误原因。运行时异常是在代码运行的时候我们才会知道的异常,比如除数为0,数组溢出等等异常。

对于开发者来说,如果使用IDE开发程序,编译时期异常可以交由IDE找出,对于运行时异常我们只能是在代码中进行处理。处理方法有两样:抛出异常和捕获异常。

关于异常的抛出和捕获

开发者可能有疑问,对于异常,我们是抛出好呢还是捕获好呢?两者没有孰优孰劣,大致处理思路是这样:如果我们知道如何处理异常就尽量不要抛出,如果不知道代码调用者可能会发生的情形就尽量抛出异常给调用者处理。诚然,有时候调用者也不知道如何处理异常,这时候个人觉得还是抛出异常给调用者好,因为这样有利于日后代码的维护和拓展。

为什要对异常进行处理

最容易想到的是保证系统的稳定性。这是其中原因之一。对异常处理,并适当输出异常日志,这样有助于我们日后对系统定位问题。还有一点个人感受特别深就是:不背他人的锅。我们在开发系统的时候,调用第三方服务的情景是非常常见的。有一次参与的项目的一个接口在前端调用失败了,翻看错误日志,发现是在调用第三方服务的时候出现网络超时。但是,第三方服务提供方调试那边的服务没有问题。如果这时候没有对调用第三方服务进行异常处理并输出日志,这锅只能自己背呗。同时也不利于错误的调试。最后确定双方的代码都没有问题,我们很容易就想到可能是服务器网络出入口的问题。找到运维人员沟通核查问题后,问题马上就定位了:确实是网络出入口问题。

对异常的正确处理于人于系统都是有很大的好处的。

Java中异常简单图谱

Java将可抛出异常分为三类:受检查异常、运行时异常(RuntimeException)、错误(Error)。所有这些异常都是Throwable的子类

  • 受检查异常:除了RuntimeException和其子类,所有的Exception都是受检查异常(编译器会检查的异常)
  • 运行时异常:RuntimeException以及子类
  • 错误:一般是系统错误,不应该捕获的异常
如何捕获异常

对于可能抛出异常的代码或者已经抛出异常的代码我们应该怎么捕获和处理呢?Java给我们提供了try...catch...finally语句:语法:

  try {
      // 捕获异常
  } catch(Exception e) {
      // 处理异常
  } finally {
      // 无论try语句是否捕获了异常,finally语句的代码都会执行
  }

下面我们看两段代码:

代码一:

    @Test
    public void testDemo() {
        String str = exception();
        System.out.println("exception方法运行结果:" + str);
    }


    private String exception() {
        int a = 0;
        int b = 1;
        try {
            int c = b / a;
            System.out.println("运算结束");
            return "b/c = " + c;
        } catch (ArithmeticException e) {
            System.out.println("catch error");
            return " error -> 发生错误了";
        } finally {
            System.out.println("我是finally语句块内容");
            return " finally block code";
        }
    }

代码二:

    @Test
    public void testDemo() {
        String str = exception();
        System.out.println("exception方法运行结果:" + str);
    }


    private String exception() {
        int a = 0;
        int b = 1;
        try {
            int c = b / a;
            System.out.println("运算结束");
            return "b/c = " + c;
        } catch (ArithmeticException e) {
            System.out.println("catch error");
            return " error -> 发生错误了";
        } finally {
            System.out.println("我是finally语句块内容");
//            return " finally block code";
        }
    }

大家想一下,这两段代码分别输出是什么,不懂多想几分钟再看答案哦。

代码一输出:

catch error
我是finally语句块内容
exception方法运行结果: finally block code

代码二输出:

catch error
我是finally语句块内容
exception方法运行结果: error -> 发生错误了

分析输出,知道不管在catch语句有没有返回语句,都会执行finally代码块的代码,执行顺序是catch->finally,如果finally有返回,就直接返回,没有就使用catch的返回。大家可以打个断点调试就知道执行顺序了。

如果catch和finally都没有返回,这时候代码就会报缺少返回值错误,需要在方法加上返回值。

 private String exception() {
        int a = 0;
        int b = 1;
        try {
            int c = b / a;
            System.out.println("运算结束");
            return "b/c = " + c;
        } catch (ArithmeticException e) {
            System.out.println("catch error");
//            return " error -> 发生错误了";
        } finally {
            System.out.println("我是finally语句块内容");
//            return " finally block code";
        }
        return "none";
    }

输出:

catch error
我是finally语句块内容
exception方法运行结果:none

当然,没有catch程序,代码还是可以继续执行:

 private String exception() {
        int a = 0;
        int b = 1;
        try {
            int c = b / a;
            System.out.println("运算结束");
            return "b/c = " + c;
        }
//        catch (ArithmeticException e) {
//            System.out.println("catch error");
////            return " error -> 发生错误了";
//        }
        finally {
            System.out.println("我是finally语句块内容");
            return " finally block code";
        }
//        return "none";
    }

输出:

我是finally语句块内容
exception方法运行结果: finally block code
关于多个异常的捕获

对于多个异常的捕获,在try后面接上多个catch语句就可以。记得,范围是从小到大,不然会编译出错。在异常抛出时,会从第一个catch语句开始匹配异常,第一个匹配到的就执行异常处理语句,后面的处理语句就不再执行了。如下代码:


    @Test
    public void testDemo() {

        String str = exception();
        System.out.println("exception方法运行结果:" + str);
    }

    private String exception() {

        try {
            int[] arr = new int[]{1, 2, 3};
            System.out.println("arr[3]=" + arr[3]);
            return "ok";
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("catch ArrayIndexOutOfBoundsException");
            return "catch ArrayIndexOutOfBoundsException";
        } catch (Exception e) {
            System.out.println("catch Exception");
            return "catch ArithmeticException";
        } finally {
            System.out.println("我是finally语句块内容");
            return "finally";
        }
    }

输出:

catch ArrayIndexOutOfBoundsException
我是finally语句块内容
exception方法运行结果:finally

关于手动抛出异常

上面讲的异常都是JVM抛出的,实际上我们还可以手动抛出异常。

  • 使用throw在语句中抛出异常
throw new RuntimeException("手动抛出RuntimeException");
  • 使用throws在方法后面抛出异常
 private String exception() throws RuntimeException {
 
     // 其它的代码
 }
关于自定义异常

有时API提供的异常类不足以满足业务需求,我们可以自定义异常。自定义异常很简单,创建一个类继承异常类或者实现异常接口就可以了。这里就不在详细介绍。抛出自定义异常和抛出API提供的异常没有什么两样。

自定义异常类,实现我们需要的构造方法就可以了。

public class CustomeException extends RuntimeException {

    public CustomeException() {
    }

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

题外话

关于异常的基础入门知识已经讲完,如果大家还有什么不懂的可以家QQ群进行讨论:599791743。欢迎大家讨论指出不足之处。

同时也欢迎大家关注微信公众号:深夜程猿,阅读更多文章。