简介
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给给用户?或者用 C 语言风格:用函数返回值作为执行状态?
Java 提供了更加优秀解决办法:异常处理机制。异常处理机制能让程序在异常发生时,按照代码预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java 中的异常可以是函数中语句执行时引发的,也可以是程序员通过 throw 语句手动抛出的,只要在 Java 程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE 就会试图寻找异常处理程序来处理异常。Throwable 类是 Java 异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或间接)实例,它才是一个异常对象,才能被异常处理机制识别。JDK 中内建了一些常用的异常类,我们也可以自定义异常。
Java 异常的分类和类结构图
Java 标准库内建了一些通用的异常,这些类以 Throwable 为顶层父类。
Throwable 又派生出 Error 类和 Exception 类。错误:Error 类以及它的子类的实例,代表 JVM 本身的错误。错误不能被程序员通过代码处理,Error 很少出现。因此,程序员应该关注 Exception 为父类的分支下的各种异常类。
异常:Exception 以及它的子类,代表程序运行时发送的各种不被期望发生的事件。可以被 Java 异常处理机制使用,是异常处理的核心。
总体上我们根据 Java 对异常的处理要求,将异常分为两类。
非检查异常(unchecked exception)
Error 和 RuntimeException 以及它们的子类。Java 在编译时,不会提示和发现这样的异常,不要求在程序中处理这些异常。所以如果愿意,我们可以编写代码处理(使用 try...catch...finally)这样的异常,也可以不处理。对于这些异常,我们更应该的不是去处理这些异常,而是应该修正代码。这样的异常发生的原因多半是代码逻辑写的有问题。如除 0 错误 ArithmeticException,错误的强制类型转换错误 ClassCastException,数组索引越界错误 ArrayIndexOutOfBoundsException,操作了空对象错误 NullPointerException 等等。检查异常(checked exception)
除了 Error 和 RuntimeException 的其它异常。Java 强制要求程序员为这样的异常做预备处理工作(使用 try...catch...finally 或者 throws)。在方法中要么用 try...catch 语句捕获它并处理,要么用 throws 子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能运行被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如 SQLException,IOException,ClassNotFoundException 等。初识异常
下面的代码会演示2个异常类型:ArithmeticException 和 InputMimatchException。前者由于整数除 0 引发,后者是输入的数据不能被转化为 int 类型引发。
package com.example;import java. util .Scanner ;public class AllDemo{ public static void main (String [] args ) { System . out. println( "----欢迎使用命令行除法计算器----" ) ; CMDCalculate (); } public static void CMDCalculate () { Scanner scan = new Scanner ( System. in ); int num1 = scan .nextInt () ; int num2 = scan .nextInt () ; int result = devide (num1 , num2 ) ; System . out. println( "result:" + result) ; scan .close () ; } public static int devide (int num1, int num2 ){ return num1 / num2 ; }}/***************************************** ----欢迎使用命令行除法计算器----0Exception in thread "main" java.lang.ArithmeticException : / by zero at com.example.AllDemo.devide( AllDemo.java:30 ) at com.example.AllDemo.CMDCalculate( AllDemo.java:22 ) at com.example.AllDemo.main( AllDemo.java:12 ) ----欢迎使用命令行除法计算器----rException in thread "main" java.util.InputMismatchException at java.util.Scanner.throwFor( Scanner.java:864 ) at java.util.Scanner.next( Scanner.java:1485 ) at java.util.Scanner.nextInt( Scanner.java:2117 ) at java.util.Scanner.nextInt( Scanner.java:2076 ) at com.example.AllDemo.CMDCalculate( AllDemo.java:20 ) at com.example.AllDemo.main( AllDemo.java:12 )*****************************************/
异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因此,只要一个函数发生了异常,那么它的所有 caller 都会被异常影响。当这些被影响的函数以异常信息输出时,就形成了异常追踪栈。
异常最先发生的地方,叫做异常抛出点。
从上面的例子可以看出,当 devide 函数发生除 0 异常时,devide 函数抛出 ArithmeticExcepton 异常,因此调用它的 CMDCalculate 函数也无法正常完成,因此也发送异常,而 CMDCalculate 的 caller --main 因为 CMDCalculate 抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的 caller 中找到最近的异常处理程序。由于这个例子没有使用任何异常处理机制,因此异常最终由 main 函数抛给 JRE,导致程序终止。
上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。
代码中选择使用 throws 声明异常,让函数的调用者去处理可能发生的异常。但是为什么只 throws 了 IOException呢?因为 FileNotFoundException 是 IOException 的子类,在处理范围内。
@Testpublic void testException() throws IOException{ //FileInputStream的构造函数会抛出FileNotFoundException FileInputStream fileIn = new FileInputStream("E:\\a.txt"); int word; //read方法会抛出IOException while((word = fileIn.read())!=-1) { System.out.print((char)word); } //close方法会抛出IOException fileIn.clos}
异常处理的基本语法
在编写代码处理异常时,对于检查异常,有2中不同的处理方式:使用 try...catch...finally 语句块处理它。或者,在函数签名中使用 throws 声明交给函数调用者 caller 去处理。
try...catch...finally 语句块try{ //try块中放可能发生异常的代码。 //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。 //如果发生异常,则尝试去匹配catch块。 }catch(SQLException SQLexception){ //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。 //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。 //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。 //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。 //如果try中没有发生异常,则所有的catch块将被忽略。 }catch(Exception exception){ //...}finally{ //finally块通常是可选的。 //无论异常是否发生,异常是否匹配被处理,finally都会执行。 //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。 //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 }
注:
- try 块中的局部变量和 catch 块中的局部变量(包括异常变量),以及 finally 中的局部变量,它们之间不可共享使用。
- 每一个 catch 块用于处理一个异常。异常匹配是按照 catch 块的的顺序从上往下寻找的,只有第一个匹配的 catch 块会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个 catch 块都有存在的意义。
- Java 中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句都不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理 catch 代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的 catch 代码块” 后面接着执行。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handing(恢复式异常处理模式)
而 Java 则是让执行流恢复到处理了异常的 catch 块后接着执行,这种策略叫做:termination model of exception handing(终结式异常处理模式)public static void main(String[] args){ try { foo(); }catch(ArithmeticException ae) { System.out.println("处理异常"); }}public static void foo(){ int a = 5/0; //异常抛出点 System.out.println("为什么还不给我涨工资!!!"); //不会执行}
throws 函数声明
throws 声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则 Java 保证你必须在方法的签名上使用 throws 关键字声明这些可能抛出的异常,否则编译不通过。throws 时另一种处理异常的方式,它不同于 try...catch...finally,throws 仅仅时将函数中可能出现的异常向调用者声明,而自己则不具体处理。
采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。
finally 块
finally 块不管异常是否发生,只要对应的 try 执行了,则它一定也执行。只有一种方法可以让 finally 块不执行:System.exit(0)
。因此 finally 块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。
注:
- finally 块没有处理异常的能力。处理异常的只能是 catch 块。
- 在同一 try...catch...finally 块中,如果 try 中抛出异常,且有匹配的 catch 块,则先执行 catch 块,再执行 finally 块。如果没有 catch 块匹配,则先执行 fianlly,然后再去外面的调用者中寻找合适的 catch 块。
- 在同一 try...catch...finally 块中,try 发生异常,且匹配的 catch 块中处理异常时抛出异常,那么后面的 finally 也会执行:首先执行 finally 块,然后去外围调用者中寻找合适的 catch 块。
throw 异常抛出语句
程序员也可以通过 throw 语句手动显示的抛出一个异常。throw 语句的后面必须是一个异常对象。
throw 语句必须写在函数中,执行 throw 语句的地方就是一个异常抛出点,它和有 JRE 自动形成的异常抛出点没有任何差别。public void save(User user){ if(user == null) throw new IllegalArgumentException("User对象为空"); //...... }
异常的链化
在一些大型的,模块化的软甲开发中,一旦一个地方发生异常,则如骨牌效应一般,将导致一连串的异常。假设 B 模块完成自己的逻辑需要调用 A 模块中的方法,如果 A 模块发生异常,则 B 也将不能完成而发生异常,但是 B 在抛出异常时,会将 A 的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。
异常链化:以一个异常对象为参数构造新的异常对象。新的异常对象将包含先前异常的信息。这项技术主要是异常类的一个带 Throwable 参数的函数来实现的。这个当作参数的异常,我们叫它根源异常(cause)。
查看 Throwable 类源码,可以发现里面有一个 Throwable 字段 cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的节点类设计如出一辙,因此形成链也是自然的了。public class Throwable implements Serializable { private Throwable cause = this; public Throwable(String message, Throwable cause) { fillInStackTrace(); detailMessage = message; this.cause = cause; } public Throwable(Throwable cause) { fillInStackTrace(); detailMessage = (cause==null ? null : cause.toString()); this.cause = cause; } //........}
自定义异常
如果要自定义异常,则扩展 Exception 类即可,因此这样的自定义异常都属于检查异常。如果要自定义非检查异常,则扩展自 RuntimeException。
自定义的异常应该总是包含如下的构造函数:- 一个无参构造函数
- 一个带有 String 参数的构造函数,并传递给父类的构造函数
- 一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数
- 一个带有 Throwable 参数的构造函数,并传递给父类的构造函数
下面是 IOException 类的完成源代码,可以借鉴。
public class IOException extends Exception{ static final long serialVersionUID = 7818375828146090155L; public IOException() { super(); } public IOException(String message) { super(message); } public IOException(String message, Throwable cause) { super(message, cause); } public IOException(Throwable cause) { super(cause); }}
异常的注意事项
- 当子类重写父类的带有 throws 声明的函数时,其 throws 声明的异常必须在父类异常的可控范围内--用于处理父类的 throws 方法的异常处理器,必须也适用于子类的这个带 throws 方法。这是为了支持多态。 例如,父类方法 throws 的是2个异常,子类就不能 throws 3个及以上的异常。父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。
class Father{ public void start() throws IOException { throw new IOException(); }} class Son extends Father{ public void start() throws Exception { throw new SQLException(); }}/**********************假设上面的代码是允许的(实质是错误的)***********************/class Test{ public static void main(String[] args) { Father[] objs = new Father[2]; objs[0] = new Father(); objs[1] = new Son(); for(Father obj:objs) { //因为Son类抛出的实质是SQLException,而IOException无法处理它。 //那么这里的try。。catch就不能处理Son中的异常。 //多态就不能实现了。 try { obj.start(); }catch(IOException) { //处理IOException } } }}
- Java 程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常会导致程序终止。如果是多线程,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。 也就是说,Java 中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。
finally 块和 return
首先一个不容易理解的事实:在 try 块中即便有 return,break,continue 等改变执行流的语句,finally 也会执行。
public static void main(String[] args){ int re = bar(); System.out.println(re);}private static int bar() { try{ return 5; } finally{ System.out.println("finally"); }}/*输出:finally*/
也就是说:try...catch.finally 中的 return 只要能执行,就都执行了,它们共同向同一个内存地址(假设地址是 0x80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。
finally 中的 return 会覆盖 try 或者 catch 中的返回值。public static void main(String[] args) { int result; result = foo(); System.out.println(result); /2 result = bar(); System.out.println(result); /2 } @SuppressWarnings("finally") public static int foo() { trz{ int a = 5 / 0; } catch (Exception e){ return 1; } finally{ return 2; } } @SuppressWarnings("finally") public static int bar() { try { return 1; }finally { return 2; } }
finally 中的 return 会抑制(消灭)前面 try 或者 catch 块中的异常
class TestException{ public static void main(String[] args) { int result; try{ result = foo(); System.out.println(result); //输出100 } catch (Exception e){ System.out.println(e.getMessage()); //没有捕获到异常 } try{ result = bar(); System.out.println(result); //输出100 } catch (Exception e){ System.out.println(e.getMessage()); //没有捕获到异常 } } //catch中的异常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception { try { int a = 5/0; return 1; }catch(ArithmeticException amExp) { throw new Exception("我将被忽略,因为下面的finally中使用了return"); }finally { return 100; } } //try中的异常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception { try { int a = 5/0; return 1; }finally { return 100; } }}
finally 中的异常会覆盖(消灭)前面 try 或者 catch 中的异常
class TestException{ public static void main(String[] args) { int result; try{ result = foo(); } catch (Exception e){ System.out.println(e.getMessage()); //输出:我是finaly中的Exception } try{ result = bar(); } catch (Exception e){ System.out.println(e.getMessage()); //输出:我是finaly中的Exception } } //catch中的异常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception { try { int a = 5/0; return 1; }catch(ArithmeticException amExp) { throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常"); }finally { throw new Exception("我是finaly中的Exception"); } } //try中的异常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception { try { int a = 5/0; return 1; }finally { throw new Exception("我是finaly中的Exception"); } }}
上面的3个例子都异于常人的编码思维,因此建议:
- 不要在 finally 中使用 return
- 不要在 finally 中抛出异常
- 减轻 finally 的任务,不要在 finally 中做一些其它的事情,finally 块仅仅用来释放资源是最合适的
- 尽量将所有的 return 写在函数的最后面,而不是 try...catch...finally 中