2
前言:说到异常体系,可能对于一些初入职场的老铁会很头痛,不能够很清晰的描述异常是个什么情况。那么本文将通过打流水仗的方式给大家介绍一下工作中涉及的异常知识。首先能看到本文,说明也对异常是有了解的,所以文章开头就通过一些概念和小例子快速熟悉一下异常,紧接着介绍异常体系,对于理解整个异常体系架构非常有帮助,最后介绍了非常非常重要的自定义异常,学会了这些,在工作中遇到的常见异常问题,处理起来一定都会游刃有余啦。

异常概述

异常

异常就是在程序的运行过程中所发生的不正常的事件,它会中断正在运行的程序

异常处理

Java 编程语言使用异常处理机制为程序提供了错误处理的能力,好处就是:如果代码中存在了异常,但是进行了捕获处理,那么程序就会继续运行下去,不会因为一个异常导致程序中断运行。

clipboard.png

例子1:如果程序可能存在异常但是没有做异常处理,那么将会导致程序不能正常的运行下去:

public class ExceptionTest {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        int c = a / b;
        System.out.println(c);
        System.out.println("输出了本句话吗");
    }
}

输出结果是:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at cn.caijiajia.cn.caijiajia.exception.ExceptionTest.main(ExceptionTest.java:12)

例子2:如果程序可能存在异常并做了异常处理,那么程序就会正常的运行下去:

public class ExceptionTest {
    public static void main(String[] args) {
        try {
            int a = 10;
            int b = 0;
            int c = a / b;
            System.out.println(c);
            System.out.println("异常代码下面的代码将都不会执行");
        } catch (ArithmeticException e) {
            System.out.println("处理异常的代码");
        }
        System.out.println("异常处理代码下面的程序将会继续执行而不会程序中断");
    }
}

输出结果是:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at cn.caijiajia.cn.caijiajia.exception.ExceptionTest.main(ExceptionTest.java:12)
处理异常的代码
异常处理代码下面的程序将会继续执行而不会程序中断
    

Java 中如何处理异常

异常处理的 5 个关键字

try:执行可能产生异常的代码
catch:捕获异常并对异常情况做相应处理
finally:无论是否发生异常,代码总能执行。(释放资源,关闭数据库连接)
throws:声明可能抛出的各种异常(受检异常较多)
throw:手动的抛出异常(手动抛出我们自定义的异常较多)

异常处理后程序运行情况

情况一:没产生异常

public void method() {
    try {
        // 代码段① [正常业务逻辑代码,此处不会产生异常]
    } catch (Exception e) {
        // 代码段② [对异常处理的代码段]
    }
    // 代码段③ [正常业务逻辑代码]
}

运行结果:
    代码段①
    代码段③

情况二:产生异常并捕获异常

public void method() {
    try {
        // 异常代码段① [正常业务逻辑代码,此处会产生异常]
        // 代码段② [正常业务逻辑代码]
    } catch (Exception e) {
        // 代码段③ [对异常处理的代码段]
    }
    // 代码段④ [正常业务逻辑代码]
}

运行结果:
    代码段①
    代码段③   
    代码段④

注意:如果想要正常的 代码段② 执行,那么可以把代码段从 trycatch 里面提出来,和 代码段④ 放在一起,当异常处理完之后,就可以同时去执行 代码段② 和 代码段④ 了。
    

情况三:产生异常并捕获异常,但是捕获异常类型不匹配产生异常类型

public void method() {
    try {
        // 异常代码段① [正常业务逻辑代码,此处会产生角标越界异常]
        // 代码段② [正常业务逻辑代码]
    } catch (IOException e) {
        // 代码段③ [对异常处理的代码段]
    }
    // 代码段④ [正常业务逻辑代码]
}

运行结果:
    代码段①
    
注意:如果捕获异常类型和产生的异常类型不匹配,那么就和没有处理异常情况一样了,trycatch 后面的代码段将都不会执行,发生异常就会导致程序中断运行

异常体系

异常类层次图

clipboard.png

Error 和 Exception

Error 类和 Exception 类的父类都是 Throwable 类

Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。因此我们在学习的时候主要是学会 Exception。

Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

Exception 异常分类及处理

Exception 异常主要分为两大类:Checked Exception,Unchecked Exception;即受检异常和非受检异常(运行时异常)


Checked Exception:受检异常,即 Java 程序必须显式处理的异常,如果程序没有处理 Checked 异常,该程序在编译时就会发生错误无法编译。例如图中所示的 IOException

处理受检异常通过有如下两种方式:
方式一:通过 trycatch 显式处理异常,否则编译不通过

public class ExceptionTest {
    public static void main(String[] args) {
        try {
            // 通过 trycatch 显示处理异常
            FileInputStream fis = new FileInputStream(new File(""));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

方式二:通过 throws 抛出异常,让上层来处理异常,否则编译不通过

public class ExceptionTest {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream(new File(""));
    }
}

Unchecked Exception:非受检异常,即 RuntimeException 运行时异常。这些异常程序可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。比如常见的 NullPointerException、IndexOutOfBoundsException 就是运行时异常。不过按照经验来说,这类异常要么尽量避免,要么出现了就要做异常处理,从而保证程序的健壮性。

处理运行时异常通过有如下两种方式:
方式一:常见的空指针处理

// 通过写伪代码来演示其处理流程
if (对象 == null) {
    // 处理对象为 null 的逻辑
} else {
    // 处理对象不为 null 的逻辑
}

方式二:跟业务相关异常,抛出自定义异常

// 通过手机号注册业务逻辑
User user = dao.getByPhone(phone);
if (user != null) {
    // 说明此手机号码已经被注册,那么就抛出业务异常(即自定义异常)
    throw new MyRuntimeException("改用户已注册");
}
// 如果没有注册,继续走注册流程代码

总结:通过本小节异常分类及处理可以发现,受检异常处理起来很简单,因为程序如果不做显式处理,那么就会编译不通过,强制要求处理;而运行时异常则是看心情处理的,但是如果想要公司代码更加健壮,更少的出现问题,最好要做一下异常处理;但是如果做这个处理呢?对于这种运行时异常,大部分都是和业务相关的,比如手机号注册例子;这种情况下在 Java 的异常体系中并没有相关异常类做处理,因为 Java 不管在智能,也不可能知道我们的业务情况,当然就不会针对业务提供一些异常类供我们使用,因此为了解决这个问题,自定义异常就出现了,它对于我们处理业务中产生的运行时异常非常非常重要,接下来就来学习自定义异常。

自定义异常

产生原因

Java 现有的异常类不能满足更多的业务异常处理,因此我们要自定义合适的异常类来处理业务异常。

如何自定义异常类

第一步:声明一个类继承 Exception 或其子类

那么我们声明的这个类到底继承谁呢?Exception?RuntimeException?
答案是:RuntimeException
原因是:看了上面的异常类层次图,应该也能发现,Exception 下面有两大类子类,受检异常和运行时异常,如果我们自定义的类继承了 Exception,则就会因为受检异常的存在而变成了受检异常类,这个时候我们自定义的异常类,如果在程序中使用,那么就必须显式处理异常,要么 trycatch,要么抛出给上层;这样一来,使得我们的程序很混乱,而且并没有达到我们预期的结果。然后当我们自定义的类继承了 RuntimeException 之后,当我们程序中想要使用的时候,直接 new 一个即可,而不再需要显式去再多做处理了。

第二步:自定义异常类应至少包含四个构造方法

public class MyException extends RuntimeException {
    public MyException() {}

    public MyException(String msg) {
        super(msg);
    }

    public MyException(Throwable throwable) {
        super(throwable);
    }

    public MyException(String msg, Throwable throwable) {
        super(msg, throwable);
    }
}

第三步:在程序中使用我们的自定义异常

public void testException() {
    User user = dao.getByPhone(phone);
    if (user != null) {
        // 因为 MyException 是继承 RuntimeException,所以这里直接抛出异常而不用做其它处理
        throw new MyException("该手机号已被注册");
    }
    // ...
} 

注:没错,这里又使用了这个手机号注册的例子,因为这个就是实实在在的在业务中的异常处理。业务是千变万化,但是它们可能产生的异常处理方式是不会变化的,按照这个思路去做异常处理即可。

tips:如上自定义的异常类中的构造方法是最基本的几个。每家公司的自定义异常可能都会有区别,比如定义一个 SuperException 类实现了 RuntimeException,然后在自定义 ClientException,ServerException 再去继承 SuperException;那么这就是一套自定义体系了,分为客户端异常和服务端异常,在需要做异常处理的地方使用对应的异常类并抛出异常错误信息即可了。再比如可能有的公司会在自定义异常类中在定义一些字段,code,msg 等,来代表某些业务码和对应的错误信息等。不管怎么样,我们只要了解自定义异常的原理后,面对哪个公司的自定义异常体系我们都能够轻松应对。

异常总结

通过本文的学习,一定要掌握两个知识,一个是异常体系,要分清受检异常和运行时异常;另一个就是自定义异常,知道如何自定义异常和如何使用自定义异常。


莼黑色
77 声望6 粉丝

人生处处皆学问!