初识

代码的世界和现实世界一样,不会一帆风顺,起码不会像我们想象中的那么顺利。写代码的是人,人的思维存在局限性,这种局限性,体现在代码中,就是漏洞。倘若一段程序在生产环境中崩溃,势必让人十分恼火。这种让人头大的情况就是异常的一种。

Java 是这样定义异常的:

异常指的是,那些在程序运行过程中,中断正常的指令流程的事件。

实际上,异常还会在以下场景中发生:

  • 用户输入错误,如:输入不符合程序预期
  • 设备错误,如:网线断开连接
  • 物理限制,如:内存不足

而我们并不希望程序因为上述情况挂掉,而是希望程序能够在遇到异常时,巧妙地处理之后,继续运行下去。

所幸,Java 提供了一套机制,用于在程序程序出错(出现异常)时,进行:

  • 报告错误
  • 保存工作结果
  • 让用户以更完善的方式退出程序

亦即:异常处理机制。顾名思义,该机制用于对程序运行中不正常的情况进行处理。
简单来说,Java 中的异常处理是:当异常发生时,运行时系统逆着方法的调用栈(call stack)去寻找能够处理这种异常的东西。

以一个把字符串转换成整数的方法为例:

public Integer stringToNumber(String strToFormat){
    Integer targetNum;
    try{
        targetNum = new Integer(strToFormat);
    }catch(NumberFormatException e){
        targetNum = DEFAULT_NUM;
    }
    return targetNum;
}

在上述代码中,当 strToFormat 不能转化成一个整形数时,就转化成一个默认的值。NumberFormatException 发生时,方法立即终止。控制权被交由 catch 代码块,也就是异常处理器(exception hanlder)。代码虽然简单,却包含了 Java 异常处理的一般步骤。

异常分类

在 Java 中,所有的异常派生于 Throwable 接口。这些异常,又被分为 ErrorException。这两类异常,又衍生了一些子类。下面的图片,描述了 Java 的异常体系图:

Java 异常体系图

不过,我们应当把精力放在 RuntimeException 上,因为“所有的运行时异常,都是程序员的问题”。这类异常,又被叫做未检查异常,通常交由 JVM 处理。相对应的,那些我们知道可能会出现问题、,被叫做已检查的异常。这类异常需要开发者手动向上级抛出,直到 JVM 级别,或者在调用方法时使用 try-catch 处理。

一般来说,在写一个方法时,要尽可能声明所有已检查异常,通过内部逻辑控制,避免未检查(运行时)异常。

异常捕获

在编码过程中,如果方法的检查异常被精心设计,那么我们只管抛出异常即可。但是,一旦调用方法的地方没有对这种可能出现的异常进行处理,那么程序就会终止。所以,要对异常进行捕获操作,正如之前的字符串转数字的方法一样。

根据经验,程序中应该捕获那些知道如何处理的异常,把“职责”外的异常,交给方法的调用者去处理。下面是一个异常捕获流程的示例代码:

    modifier returnType someMethod(type1 param1, type2 param2) throws Exception1, Exceptio2{
    
        try{
            ① // 某些操作
            return returnType value;
        }catch(Exceptionx | Exceptiony e){
            ② // 处理异常
        }catch(Excpetion1 e1){
            ③ throw new Excption1(e1);
        }catch(Exception e2){
            ④ throw e2;
        }finally{
            ⑤ // 一些操作
            return returnType mockValue;
        }
        
}   

在上述代码中,① 中进行操作,捕获了 Exeptionx 或者 Exceptiony 时,② 进行异常处理;捕获了 Exception1 时, ③ 把该异常包装;捕获了 Exception 2 时,④ 直接抛出异常;无论有无异常发生,⑤ 都会执行。

上面的代码还有一个问题,就是 finally 代码中包含返回值。有以下规则:

  • try 代码块中包含 return 语句,finally 语句中的代码也会被执行;若 finally 中也包含 return 语句,则会替代 try 中的语句

使用异常的技巧

  • 不要过分细分异常,能用一个 try 块处理的尽量用一个,然后按层次捕获异常,下面的代码是被建议的:
try{
    // 某些操作
}catch(SomeException1 e1){
    // 异常 1 的处理
}catch(SomeException2 e2){
    // 异常 2 的处理
}
  • 适当地转化异常,如:可以把一个 NumberFormatException 转化成自定义的异常
  • 遇到异常情况,可以更加严格一点。如:抛出 EmptyStackException 比 NullPointerException 更加语义化
  • 学会把异常传递,即:「早抛出,晚捕获」

野原英雄
206 声望25 粉丝

一只小猿