10 Java 异常

异常指的是程序运行时出现的不正常情况。程序运行过程中难免会发生异常,发生异常并不可怕,程序员应该考虑到有可能发生这些异常,编程时应能正确的处理异常,使成为健壮的程序。

异常是相对于 return 的一种退出机制,可以由系统触发,也可以由程序通过 throw 语句触发,异常可以通过 try/catch 语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。

异常的层次

Java 的异常类是处理运行时的特殊类,每一种异常对应一种特定的运行错误.所有Java异常类都是系统类库中 Exception 类的子类。

异常类继承层次图

Throwable 类

所有的异常类都直接或间接地继承于 java.lang.Throwable 类,在 Throwable 类有几个非常重要的方法:

  • String getMessage():获得发生异常的详细消息。
  • void printStackTrace():打印异常堆栈跟踪信息。
  • void printStackTrace(PrintStream s) 通常用该方法将异常内容保存在日志文件中,以便查阅。
  • String toString():获得获取异常类名和异常信息的描述。
public class Throwable implements Serializable {
    ...

    public String getMessage() {
        return detailMessage;
    }

    public void printStackTrace() {
        printStackTrace(System.err);
    }

    public void printStackTrace(PrintStream s) {
        printStackTrace(new WrappedPrintStream(s));
    }

    public String toString() {
        String s = getClass().getName();
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s;
    }
    ...
}

Error 和 Exception

Throwable 有两个直接子类:Error 和 Exception。

Error 是程序无法恢复的严重错误,程序员根本无能为力,程序中不能对其编程处理, 对 Error 一般不编写针对性的代码对其进行处理 只能让程序终止。例如:JVM 内部错误、内存溢出和资源耗尽等严重情况。

Exception 是程序可以恢复的异常,它是程序员所能掌控的。例如:除零异常、空指针访问、网络连接中断和读取不存在的文件等。

受检查异常和运行时异常

Java 的异常处理机制会区分两种不同的异常类型:已检异常 checked 和未检异常 unchecked。

已检异常

在明确的特定情况下抛出,经常是应用能部分或完全恢复的情况。例如,某段代码要在多个可能的目录中寻找配置文件。如果试图打开的文件不在某个目录中,就会抛出 FileNotFoundException 异常。在这个例子中,我们想捕获这个异常,然后在文件可能出现的下一个位置继续尝试。也就是说,虽然文件不存在是异常状况,但可以从中恢复,这是意料之中的失败。

非受检异常

在 Java 环境中有些失败是无法预料的,这些失败可能是由运行时条件或滥用库代码导致的。例如把无效的 null 传给使用对象或数组的方法,会抛出 NullPointerException 异常。基本上任何方法在任何时候都可能抛出未检异常。这是 Java 环境中的墨菲定律:“会出错的事总会出错。”从未检异常中恢复,虽说不是不可能,但往往很难,因为完全不可预知。运行时异常往往是程序员所犯错误导致的,健壮的程序不应该发生运行时异常

若想区分已检异常和未检异常,记住两点:异常是 Throwable 对象,而且异常主要分为两类,通过 Error 和 Exception 子类标识。只要异常对象是 Error 类,就是未检异常。Exception 类还有一个子类 RuntimeException , RuntimeException 类的所有子类都属于未检异常。除此之外,都是已检异常。

提示:对于运行时异常通常不采用抛出或捕获处理方式,而是应该提前预判,防止这种发生异常,做到未雨绸缪。例如在进行除法运算之前应该判断除数是非零的,修改代码进行提前预判这样处理要比通过 try-catch 捕获异常要友好的多。

对比受检和未受检异常

通过以上介绍可以看出,未受检异常和受检异常的区别如下:受检异常必须出现在 throws 语句中,调用者必须处理,Java 编译器会强制这一点,而未受检异常则没有这个要求。

为什么要有这个区分呢?我们自己定义异常的时候应该使用受检还是未受检异常呢?对于这个问题,业界有各种各样的观点和争论,没有特别一致的结论。

一种普遍的说法是:未受检异常表示编程的逻辑错误,编程时应该检查以避免这些错误,比如空指针异常,如果真的出现了这些异常,程序退出也是正常的,程序员应该检查程序代码的 bug 而不是想办法处理这种异常。受检异常表示程序本身没问题,但由于 I/O、网络、数据库等其他不可预测的错误导致的异常,调用者应该进行适当处理。

但其实编程错误也是应该进行处理的,尤其是 Java 被广泛应用于服务器程序中,不能因为一个逻辑错误就使程序退出。所以,目前一种更被认同的观点是:Java 中对受检异常和未受检异常的区分是没有太大意义的,可以统一使用未受检异常来代替。

这种观点的基本理由是:无论是受检异常还是未受检异常,无论是否出现在 throws 声明中,都应该在合适的地方以适当的方式进行处理,而不只是为了满足编译器的要求盲目处理异常,既然都要进行处理异常,受检异常的强制声明和处理就显得烦琐,尤其是在调用层次比较深的情况下。

其实观点本身并不太重要,更重要的是一致性,一个项目中,应该对如何使用异常达成一致,并按照约定使用。

常见异常

Exception 类有若干子类,每个子类代表一种特定的运行错误,这些子类有的是系统事先定义好并包含在 Java 类库中的,成为系统定义的运行异常。

  • ClassNotFoundException 未找到要装载的类
  • ArrayIndexOutOfBoundsException 数组越界访问
  • FileNotFoundException 文件找不到, checked异常
  • IOException 输入, 输出错误, checked 异常
  • NullPointerException 空指针异常, unchecked 异常
  • ArithmeticException 算术运算错误
  • InterruptedException 中断异常, 线程在进行暂停处理时(如睡眠)被调度打断将引发该异常

异常的处理

  • 对待受检查异常。如果当前方法有能力解决,则捕获异常进行处理;没有能力解决,则抛出给上层调用方法处理。
  • 涉及了五个关键字 try catch finally throw throws
  • try...catch..finally 或者 try-with-resources(Java7增加)语句结构进行捕获
  • try 必须带有 catch 或者 finally,两者至少二选一或者资源声明才可以使用。
  • 一个 try 可以引导多个 catch 块。但是不要定义多余的 catch 块,多个 catch 块的异常出现继承关系,父类异常 catch 块放在最后面。
  • 异常发生后,try 块中的剩余语句将不再执行。
  • catch 块中的代码要执行的条件是,首先在 try 块中发生了异常,其次异常的类型与 catch 要捕捉的一致。 建议声明更为具体的异常,这样处理的可以更具体。当捕获的多个异常类之间存在父子关系时,捕获异常顺序与 catch 代码块的顺序有关。一般先捕获子类,后捕获父类,否则子类捕获不到。
  • 可以无 finally 部分,但如果存在,则无论异常发生否,finally 部分的语句均要执行。即便是 try 或 catch 块中含有退出方法的语句 return,也不能阻止 finally 代码块的执行; 除非执行 System.exit(0) 等导致程序停止运行的语句。

try-catch 不仅可以嵌套在 try 代码块中,还可以嵌套在 catch 代码块或 finally 代码块,finally 代码块后面会详细介绍。try-catch 嵌套会使程序流程变的复杂,如果能用多catch捕获的异常,尽量不要使用 try catch 嵌套。特别对于初学者不要简单地使用 Eclipse 的语法提示不加区分地添加 try-catch 嵌套,要梳理好程序的流程再考虑 try-catch 嵌套的必要性。

传统处理

try {语句块;} 
catch (异常类名   参变量名) {语句块;}   
finally {语句块;} // 定义一定执行的代码:通常用于关闭资源

Java 7 推出了多重捕获(multi-catch)技术, 可以把这些异常合并处理

try {
    // 可能会发生异常的语句
} catch (IOException | ParseException e) {
    // 调用方法 methodA 处理
}

finally 代码块

try-catch 语句后面还可以跟有一个 finally 代码块,try-catch-finally 语句语法如下:

注意:为了代码简洁等目的,可能有的人会将 finally 代码中的多个嵌套的try-catch 语句合并。每一个close()方法对应关闭一个资源,如果某一个 close() 方法关闭时发生了异常,那么后面的也不会关闭,因此这种代码是有缺陷的。

释放资源

有时在 try-catch 语句中会占用一些非 Java 资源,如:打开文件、网络连接、打开数据库连接和使用数据结果集等,这些资源并非 Java 资源,不能通过 JVM 的垃圾收集器回收,需要程序员释放。为了确保这些资源能够被释放可以使用 finally 代码块或 Java 7 之后提供自动资源管理(Automatic Resource Management)技术。

自动资源管理

使用 finally 代码块释放资源会导致程序代码大量增加,一个 finally 代码块往往比正常执行的程序还要多。在Java 7之后提供自动资源管理(Automatic Resource Management)技术,可以替代 finally 代码块,优化代码结构,提高程序可读性。

自动资源管理是在 try 语句上的扩展,语法如下:

try (声明或初始化资源语句) {
    //可能会生成异常语句
} catch(Throwable e1){
    //处理异常e1
} catch(Throwable e2){
    //处理异常e1
} catch(Throwable eN){
    //处理异常eN
}

在 try 语句后面添加一对小括号“()”,其中是声明或初始化资源语句,可以有多条语句语句之间用分号“;”分隔。

示例代码如下:

public class HelloWorld {

    public static void main(String[] args) {
        Date date = readDate();
        System.out.println("读取的日期  = " + date);
    }

    public static Date readDate() {
        // 自动资源管理
        try (FileInputStream readfile = new FileInputStream("readme.txt");     
                InputStreamReader ir = new InputStreamReader(readfile);        
                BufferedReader in = new BufferedReader(ir)) {                  

            // 读取文件中的一行数据
            String str = in.readLine();
            if (str == null) return null;

            DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
            Date date = df.parse(str);
            return date;
        } catch (IOException e) {
            System.out.println("处理IOException...");
            e.printStackTrace();
        } catch (ParseException e) {
            System.out.println("处理ParseException...");
            e.printStackTrace();
        }
        return null;
    }
}

注意 所有可以自动管理的资源需要实现 AutoCloseable 接口,上述代码中三个输入流 FileInputStream、InputStreamReader 和 BufferedReader 从 Java 7之后实现 AutoCloseable 接口,具体哪些资源实现 AutoCloseable 接口需要查询 API文档。

资源的声明和初始化放在 try 语句内,不用再调用 finally,在语句执行完 try语句后,会自动调用资源的 close() 方法。资源可以定义多个,以分号分隔。在 Java 9 之前,资源必须声明和初始化在 try 语句块内,Java 9去除了这个限制,资源可以在try语句外被声明和初始化,但必须是 final 的或者是事实上 final 的(即虽然没有声明为final但也没有被重新赋值)。

finally 语句有一个执行细节,如果在 try 或者 catch 语句内有 return 语句,则 return 语句在 finally 语句执行结束后才执行,但 finally 并不能改变返回值,我们来看下面的代码。

public static int yyy() {
    int ret = 1;
    try {
        return ret;
    } finally {
        ret = 2;
    }
}

输出结果为 1。

如果在 finally 中也有 return 语句呢?try 和 catch 内的 return 会丢失,实际会返回 finally 中的返回值。

如果在 finally 中也有 return 语句呢?try 和 catch 内的 return 会丢失,实际会返回 finally 中的返回值。所以,一般而言,为避免混淆,应该避免在 finally中使用 return 语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。

throws 与声明方法抛出异常

在一个方法中如果能够处理异常,则需要捕获并处理。但是本方法没有能力处理该异常,捕获它没有任何意义,则需要在方法后面声明抛出该异常,通知上层调用者该方法有可以发生异常。

注意:如果声明抛出的多个异常类之间有父子关系,可以只声明抛出父类。但如果没有父子关系情况下,最好明确声明抛出每一个异常,因为上层调用者会根据这些异常信息进行相应的处理。假如一个方法中有可能抛出 IOException 和 ParseException 两个异常,那么声明抛出 IOException 和 ParseException 呢?还是只声明抛出 Exception 呢?因为 Exception 是 IOException 和 ParseException 的父类,只声明抛出 Exception 从语法是允许的,但是声明抛出 IOException 和 ParseException 更好一些。

  • 使用 throw 抛出异常. 异常的本质是对象因为 throw 关键词后跟的是 new 运算符来创建的一个异常对象。
  • 使用 throws 关键字抛出一个或多个异常。

自定义异常

有些公司为了提高代码的可重用性,自己开发了一些 Java 类库或框架,其中少不了自己编写了一些异常类。实现自定义异常类需要继承 Exception 类或其子类,如果自定义运行时异常类需继承 RuntimeException 类或其子类。

我们通过继承 Exception 或者 RuntimeException 来定义一个异常。

Java 内部定义 WebServiceException 示例:

package javax.xml.ws;

public class WebServiceException extends java.lang.RuntimeException {

  public WebServiceException() {
    super();
  }

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

  public WebServiceException(String message, Throwable cause) {
    super(message,cause);
  }

  public WebServiceException(Throwable cause) {
    super(cause);
  }

}

和很多其他异常类一样,我们没有定义额外的属性和代码,只是继承了Exception,定义了构造方法并调用了父类的构造方法。

throw 与显式抛出异常

通过 throw 语句显式抛出异常, 显式抛出异常目的有很多,例如不想某些异常传给上层调用者,可以捕获之后重新显式抛出另外一种异常给调用者。

注意:throw 显式抛出的异常与系统生成并抛出的异常,在处理方式上没有区别,就是两种方法:要么捕获自己处理,要么抛出给上层调用者。

设计良好异常机制

  • 考虑要在异常中存储什么额外状态——记住,异常也是对象;
  • Exception 类有四个公开的构造方法,一般情况下,自定义异常类时这四个构造方法都要实现,可用于初始化额外的状态,或者定制异常消息;
  • 不要在你的 API 中自定义很多细致的异常类——Java I/O 和反射 API 都因为这么做了而受人诟病,所以别让使用这些包时的情况变得更糟;
  • 别在一个异常类型中描述太多状况——例如,实现 JavaScript 的 Nashorn 引擎(Java 8 新功能)一开始有超多粗制滥造的异常,不过在发布之前修正了。

异常在子类覆盖中的体现

  1. 子类覆盖父类时, 如果父类的方法抛出的异常,那么子类只能抛出父类异常或该异常的子类.
  2. 如果父类方法抛出多个异常, 那么子类在覆盖方法时,只能抛出父类异常的子集.
  3. 如果父类或接口的方法中没有异常抛出, 那么子类在覆盖方法时,也不可能抛出异常.如果子类方法发生异常,就必须进行 try 处理,绝对不能抛.

一句话就是父类限制了子类可抛出的异常。

有争议的两种处理异常的反模式

// 不要捕获异常而不处理
try {
    someMethodThatMightThrow();
} catch(Exception e){
}

// 不要捕获,记录日志后再重新抛出异常
try {
    someMethodThatMightThrow();
} catch(SpecificException e){
    log(e);
    throw e;
}

第一个反模式直接忽略近乎一定需要处理的异常状况(甚至没有在日志中记录)。这么做会增大系统其他地方出现问题的可能性——出现问题的地方可能会离原来的位置很远。除非真的确定即使爆出异常,则可以忽略。

第二个反模式只会增加干扰——虽然记录了错误消息,但没真正处理发生的问题——在系统高层的某部分代码中还是要处理这个问题。一般的操作是在catch 块中比如进行资源关闭的工作,然后可重新抛出异常 throw e。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容