跳至主要內容

异常机制详解

Quest大约 6 分钟基础知识异常类

什么是异常

异常是指程序运行过程中由于外部问题或程序逻辑错误破坏程序正常运行流程的事件。引发异常的问题可以是程序逻辑的错误,也可以是资源空间不足导致,Java中程序异常会抛出对应的异常对象。

异常分类

异常类层次结构图
异常类层次结构图

Throwable

Java中,所有的异常都有一个共同的父类ThrowableThrowable类包含两个重要的子类Exception类和Error类。Throwable类中有两个主要参数:message表示异常消息,cause便是触发该异常的其他异常,异常可以形成异常链,上层的异常由底层触发,cause表示底层异常。

Throwable常用方法:
void printStackTrace():打印异常栈信息到控制台
String getMessage():获取异常发生的简要信息
String getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
String toString():返回异常发生时的详细信息
StackTraceElement[] getStackTrace():获取异常栈每一层的信息,每个StackTraceElement包括文件名、类名、函数名、行号等信息。

Exception

程序本身可以捕获并且可以处理的异常,Exception又可以分为Checked Exception受检异常和Unchecked Exception非受检异常。

  • 受检异常

即受检查异常,开发过程中需要catchthrow的异常,如果没有进行处理,代码无法通过编译,作为一种强制规范。除了RuntimeException类及其子类以外,其他Exception类及其子类都为受检查异常,常见的受检异常包含IOExceptionClassNotFoundException

  • 非受检异常

代码在编译时,不会进行检查,这类异常为运行时异常,一般是有代码逻辑错误引起,程序中可以显式捕获异常,也可以不进行捕获。RuntimeException类及其子类都属于非受检异常,常见的非受检异常包含NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)、ClassCastException(类型转换异常)、ArithmeticException(算数异常)。

Error

Error表示系统级的错误,是运行内部环境错误或者硬件相关的问题,属于程序无法处理恢复的错误,例如Virtual MachineError(Java虚拟机运行错误)、OutOfMemoryError(虚拟机内存不够错误),这些异常发生时,JVM会选择线程终止。

自定义异常

除了Java提供的异常类,也可以自己定义异常类,通过继承Exception或者它的某个子类,比如继承RuntimeException或它的某个子类,则定义的异常也是非受检异常;如果是Exception类的其他子类,则自定义异常为受检异常。
下面是自定义异常示例:

public class BusinessException extends Exception {

    public BusinessException(String code, String message) {
        super("[code=" + code + ", msg=" + message + "]");
    }
}

异常处理机制

异常捕获

try-catch-finally

try块:需要被捕获异常的代码块,后面可以不接或接多个catch块,如果不接catch块,则必须接一个finally块。
catch块:处理捕获到异常,同一个catch中也可以处理多个异常,用隔开。
finally块:无论是否捕获或处理异常,最终都会执行的代码块,当try块或catch块中包含return语句时,会先执行finally块中语句后再返回。

try {
    System.out.println("try");
    throw new NullPointerException();
}catch (NullPointerException | IndexOutOfBoundsException e1){
    System.out.println("catch");
}catch (ArithmeticException e2){
    // handle IOException
} finally {
    System.out.println("finally");
}

注意

finally块中不要使用return

如果try语句中有return语句并且执行后,不会直接返回,会继续执行finally块中代码,如果块中存在return语句,会在此直接返回,并忽略try语句中的return值。

finally块中代码可能不会执行

前面代码调用System.exit()退出程序、程序所在的线程死亡、关闭CPU。

try-with-resources

对于部分使用资源的场景,首先是打开资源再通过finally块关闭资源,Java7提供try-with-resources语法优化使用资源场景的异常捕获,这种语法适用于java.lang.AutoCloseable或者java.io.Closeable


//使用try-catch-finally捕获有关闭资源的场景
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
    fileInputStream = new FileInputStream(file);
    fileOutputStream = new FileOutputStream("D://result.txt");
    //code
}catch (IOException e){
    e.printStackTrace();
}finally {
    if (fileInputStream != null){
        fileInputStream.close();
    }
    if(fileOutputStream != null){
        fileOutputStream.close();
    }
}

//使用try-with-resources
try (FileInputStream fileInputStream = new FileInputStream(file);
     FileOutputStream fileOutputStream = new FileOutputStream(outPath)){
    //code
}catch (IOException e){
   e.printStackTrace();
}

注意

面对必须要关闭的资源,应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短清晰。产生的异常对我们也更有用。

异常声明

throws关键字用于声明方法可能抛出的异常,throws跟在方法括号后面,可以同时声明多个异常,以逗号隔开。异常声明表示是该方法可能会抛出这些异常,调用者需要进行对方法异常进行捕获或继续抛出。

throws主要针对受检异常的抛出,如果方法中存在受检异常并且没有进行捕获,则需要通过throws将异常抛出。非受检异常不需要throws进行声明,但在运行时异常会被系统抛出。

    /**
     *
     * @throws IOException IO异常
     * @throws SQLException 操作数据库异常
     */
    public void readFile(File file) throws IOException, SQLException {
        FileInputStream fileInputStream = new FileInputStream(file);
    }

抛出异常

throw关键字用于手动抛出异常,多数情况都不需要手动抛出异常,但是需要统一异常向外暴露时,可以先捕获异常再使用throw将异常抛出。

    public void readFile(File file) throws BusinessException {
        try (FileInputStream fileInputStream = new FileInputStream(file);){
            //code
        }catch (IOException e){
            BusinessException ex = new BusinessException("B0001","read file failed");
            e.initCause(ex);
            throw ex;
        }
    }

异常实践

异常使用仅限于异常情况

异常应该仅用于异常情况,异常不能代替正常的条件判断,比如,对于一个引用变量,正常情况下它的值可能为null,就应该先检查是不是null,不为null的情况下再进行调用。阿里Java开发手册中规定:“【强制】 Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理, 比如: NullPointerExceptionIndexOutOfBoundsException 等等”。

不要同时日志打印和抛出异常

不要打印异常日志的同时又抛出异常,这两个任取其中一个。

try {
    Integer.valueOf("xyz");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

不要忽略异常

不要捕获了异常不做任何处理和日志记录,如果代码运行产生异常,则无法拿到足够的错误信息去定位问题,如果不想处理异常,将异常抛出给调用者,由调用者进行处理。

常见异常

常见的异常真要针对非受检异常,即运行时产生的异常:
java.lang.NullPointerException:空指针异常
java.lang.ArithmeticException:算术条件异常
java.lang.ClassNotFoundException:找不到类异常
java.lang.ArrayIndexOutOfBoundsException:数组下标越界异常
java.lang.IllegalArgumentException:非法参数异常
java.lang�.ClassCastException:类型转换异常
java.lang.NumberFormatException:字符串转换为数字格式异常