1 java异常继承体系
1.1 常用异常继承体系
Java把所有的非正常情况分为两种情况:异常(Exception)和错误(Error),它们都继承Throwable父类。
Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。
1.2 Checked异常和Runtime异常
Java异常(Exception)被分为两大类:Checked异常和Runtime异常。
所有的RuntimeException类及其子类的实例都被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。
Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常,如果程序没有处理Checked异常,则程序在编译时会发生错误,无法通过编译;而Runtime异常无须处理。
对于Checked异常的处理方式有两种:
使用try.....catch块来捕获该异常。当前方法明确知道如何处理该异常,然后在对应的catch块中修复该异常。
在定义方法时,使用throws关键字抛出该异常。当前方法一般不知道如何处理这种异常。
只有Java语言提供了Checked异常,其他语言都没有,Checked异常体现了Java的严谨性,它要求程序员必须注意该异常:要么显式声明抛出,要么显式捕获异常并处理它,总之不运行对Checked异常不闻不问。这是一种非常严谨的设计哲学,可以增加程序的健壮性。
问题是:大部分的方法总是不能明确知道如何处理异常,因此只能声明抛出该异常,而这种情况又是如此普遍,所以Changpengsun异常降低了程序开发的生产率和代码的执行效率。关于Checked异常的优劣,在Java领域是一个备受争议的问题。
Checked VS Runtime 示例:
package com.demo4;
public class Test {
public static void main(String[] args) {
// #1Checked
catchChecked();
// #2Checked
// 此处try...catch不可省略
try {
throwChecked();
} catch (Exception e) {
}
// #3Runtime
// 此处的Runtime异常交给main方法,最终交给JVM
throwRuntime();
// #4Runtime
// 此处可以捕获到Runtime异常
// 此处try...catch可省略。如果省略,则跟#3完全相同
try {
throwRuntime();
} catch (Exception e) {
}
}
public static void catchChecked() {
try {
// 抛出Checked异常
// 该代码必须处于try块里,或者处于带throws声明的方法中
throw new Exception();
} catch (Exception e) {
}
}
public static void throwChecked() throws Exception {
// 抛出Checked异常
// 该代码必须处于try块里,或者处于带throws声明的方法中
throw new Exception();
}
public static void throwRuntime() {
// 抛出Runtime异常
// 既可以用try..catch捕获该异常或者方法带有throws声明
// 也可以完全不理会该异常
throw new RuntimeException();
}
}
1.3 常用异常
ArithmeticException:当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
IndexOutOfBoundsException:用非法索引访问排序集合(例如,数组、字符串等)时抛出的异常,如果索引为负或大于等于排序集合大小,则该索引为非法索引。
NullPointerException:空指针异常。当应用程序试图在需要对象的地方使用了null 时,抛出该异常。
ClassNotFoundException:无法找到指定的类异常。
CloneNotSupportedException:相关类没有继承Cloneable接口。
IOException:发生 I/O 错误时引发的异常。
FileNotFoundException:当java程序试图打开指定路径名表示的文件失败时,抛出此异常。
EOFException:当输入过程中意外到达文件或流的末尾时,抛出此异常。此异常主要被数据输入流用来表明到达流的末尾。
UnknownHostException:给定IP的主机无法连接。
1.4 异常对象常用方法
异常类Throwable实现了绝大多数的方法,Error和Exception类只是将构造函数进行了重写。
Throwable类的属性主要有:
private StackTraceElement[] stackTrace:包含了线程执行时堆栈快照的堆栈跟踪数据。
private String detailMessage:异常的详细文字描述
private Throwable cause:导致当前异常抛出的“原因(cause)异常”,用于异常链的实现。
Throwable类构造方法主要有:
public Throwable():构造一个将 null 作为其详细消息的新 throwable。
public Throwable(String message) :构造带指定详细消息的新 throwable。
public Throwable(Throwable cause):构造一个带指定 cause 和 (cause==null ? null :cause.toString())(它通常包含类和 cause 的详细消息)的详细消息的新 throwable。
public Throwable(String message, Throwable cause):构造一个带指定详细消息和 cause 的新 throwable。
Throwable类方法主要有:
public StackTraceElement[] getStackTrace():提供由 printStackTrace() 输出的堆栈跟踪信息的编程访问。
public void printStackTrace():将此 throwable 及其相关异常信息输出到错误流(System.err) 。
public void printStackTrace(PrintStream s) :将此 throwable 及其相关异常信息输出到指定的 PrintStream。
public void printStackTrace(PrintWriter s):将此 throwable 及其相关异常信息输出到指定的 PrintWriter。
public String getMessage():返回此 throwable 的详细描述信息。
public synchronized Throwable initCause(Throwable cause):将此 throwable 的 cause 初始化为指定值。
public synchronized Throwable getCause():返回此 throwable 的 cause;如果 cause 不存在或未知,则返回 null。
Error和Exception类同样包含上述属性和方法,同时构造函数对应重写。
2 异常关键字
Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字,其中try关键字后紧跟一个花括号括起来的代码块(花括号不能省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制保证finally块总被执行。throws关键字主要用于方法签名中,用于声明该方法可能抛出的异常;throw用于抛出一个实际的具体的异常对象,throw可以单独作为语句使用。
2.1 try catch finally
特性:
try、catch、finally的花括号不能省略,即使里面只有一个语句。
三者可以try.....catch、try.....finally、try.....catch.....finally三种组合
可以有多个catch,在捕获异常时,先捕获小异常,再捕获大异常。
try块内定义的局域变量作用域只是try块,不能在catch和finally块中使用。
2.2 throw throws
2.2.1 throws
如果当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理,则使用throws声明抛出异常;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行。
throws声明只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开。throws声明抛出的语法格式如下:
throws ExceptionClass1,ExceptionClass2.......
如果一个方法用throws声明抛出了一个异常类型,那么该方法内就无须使用try....catch块来捕获该类型的异常了,而是由它的调用者方法选择合适的方式处理异常。
使用throws声明抛出异常时,有一个限制,就是方法重写时的一条规则:子类方法声明抛出的异常类型应该是父类方法抛出异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
正是因为Checked异常要么需要try...catch,代码烦琐,要么throws声明抛出,方法重写有限制,所以推荐使用Runtime异常。
当使用Runtime异常时,程序无须在方法中声明抛出异常,只管直接抛出Runtime异常即可。在合适的地方再捕获该Runtime异常并对异常进行处理即可。
2.2.2 throw
当程序出现错误时,系统会自动抛出异常;除此之外,java也运行程序主动抛出异常,主动抛出异常使用throw语句来完成。
throw语句可以单独使用,抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。格式如下:
throw ExceptionInstance;
如果抛出的是Checked异常,则该throw语句要么处于try块中,要么放在一个带有throws声明异常抛出的方法中。如果throw语句抛出的是Runtime异常,则可以放在try块或者throws声明异常抛出,也可以不做任何处理,把异常交给该方法的调用者处理。例如,“1.2 Checked异常和Runtime异常”中的示例代码。
2.3 return与finally
首先需要明确finally的特性:
不管有木有出现异常,finally块中代码都会执行;
当try和catch中有return时,finally仍然会执行;
finally是在return后面的表达式执行后,才运行的,return表达式的值被暂存。
所以,finally块中是否也存在return语句,会对try或catch块中return语句的暂存结果产生不同影响。
示例公用类:
package com.demo5;
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "姓名: " + this.name;
}
}
2.3.1 try{ return; }catch(){} finally{} return;
程序执行try块中return之前代码;return语句中的表达式执行并且运算结果被暂存;再执行finally块;最后返回try块中被暂存的运算结果;finally块之后的语句return,因为程序在try中已经return,所以不再执行。
示例1(return 基本类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static int testFinally() {
int i = 0;
try {
i = 1;
return i;// 该处返回值被暂存
} catch (Exception e) {
i = 2;
} finally {
i = 3;
}
i = 4;
return i;
}
}
运行结果:
i值: 1
示例2(return 引用类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static Person testFinally() {
Person i = new Person();
try {
i.setName("sun1");
return i;
} catch (Exception e) {
i.setName("sun2");
} finally {
i.setName("sun3");
}
return i;
}
}
运行结果:
i值: 姓名: sun3
2.3.2 try{ return; }catch(){} finally{return;}
程序执行try块中return之前代码;return语句中的表达式执行并且运算结果被暂存;再执行finally块;因为finally块中有return,所以返回finally块中return的结果,并退出。
示例1(return 基本类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static int testFinally() {
int i = 0;
try {
i = 1;
return i;// 该处返回值被暂存
} catch (Exception e) {
i = 2;
} finally {
i = 3;
return i;
}
}
}
运行结果:
i值: 3
示例2(return 引用类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static Person testFinally() {
Person i = new Person();
try {
i.setName("sun1");
return i;
} catch (Exception e) {
i.setName("sun2");
} finally {
i.setName("sun3");
return i;
}
}
}
运行结果:
i值: 姓名: sun3
2.3.3 try{ } catch(){return;} finally{} return;
先执行try块中语句;
如果没有异常:执行完try块再finally块再return。
如果有异常:程序执行catch块中return之前代码;catch块中,return语句后的表达式执行并且运算结果被暂存;再执行finally块; 最后返回catch块中被暂存的运算结果;finally块之后的语句return,因为程序在catch块已经return,所以不再执行。
示例1(return 基本类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static int testFinally() {
int i = 0;
try {
i = 1;
getRuntimeException();
} catch (Exception e) {
i = 2;
return i;
} finally {
i = 3;
}
i = 4;
return i;
}
static void getRuntimeException() {
throw new RuntimeException();
}
}
运行结果:
i值: 2
示例2(return 引用类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static Person testFinally() {
Person i = new Person();
try {
i.setName("sun1");
getRuntimeException();
} catch (Exception e) {
i.setName("sun2");
return i;
} finally {
i.setName("sun3");
}
return i;
}
static void getRuntimeException() {
throw new RuntimeException();
}
}
运行结果:
i值: 姓名: sun3
2.3.4 try{} catch(){return;}finally{return;}
先执行try块中语句;
如果没有异常:
执行完try块再finally块中的return。
如果有异常:
程序执行catch块中return之前代码;catch块中,return语句后的表达式执行并且运算结果被暂存;再执行finally块;因为finally块中有return,所以返回finally块中return的结果,并退出。
示例1(return 基本类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static int testFinally() {
int i = 0;
try {
i = 1;
getRuntimeException();
} catch (Exception e) {
i = 2;
return i;
} finally {
i = 3;
return i;
}
}
static void getRuntimeException() {
throw new RuntimeException();
}
}
运行结果:
i值: 3
示例2(return 引用类型):
package com.demo5;
public class Test {
public static void main(String[] args) {
System.out.println("i值: " + testFinally());
}
static Person testFinally() {
Person i = new Person();
try {
i.setName("sun1");
getRuntimeException();
} catch (Exception e) {
i.setName("sun2");
return i;
} finally {
i.setName("sun3");
return i;
}
}
static void getRuntimeException() {
throw new RuntimeException();
}
}
运行结果:
i值: 姓名: sun3
2.3.5 try{ return;}catch(){return;} finally{}
2.3.1和2.3.3两种情况的合并。
2.3.6 try{ return;}catch(){return;} finally{return;}
2.3.2和2.3.4两种情况的合并。
总结:
在try catch块里return的时候,finally也会被执行。
return语句会把后面的值暂存一份用来返回。如果return的是基本类型的,finally里对暂存变量的改动将不起效果,如果return 的是引用类型的,改动可以有效果。
finally里的return语句会把try catch块里的return语句效果给覆盖掉。
建议:
不要在finally块中使用return,编译器会产生warning提示。
尽量不要在try和catch中使用return,要在方法尾使用return。
如果在try catch块里return, 则不要在finally块里操作被return的变量。
3 JDK 7异常增强
3.1 自动关闭资源
JDK 7之前读写文件:
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上面的代码臃肿。JDK 7增强了try关键字:允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接,IO连接等)。try语句在该语句结束时自动关闭这些资源。
为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口,就必须实现close( )方法。JDK 7 几乎把所有的“资源类”(包括文件IO的各种类,JDBC的Connection、Statement等接口......)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。
上述示例代码可以改写为:
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("a.txt");) {
} catch (IOException e) {
e.printStackTrace();
}
}
自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源)。
3.2 多异常捕获
在JDK 7以前,每个catch块只能捕获一种类型的异常;但是从JDK 7开始,一个catch块可以捕获多个类型的异常。
使用一个catch块捕获多种异常时,需要注意如下两个地方:
捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开
捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
示例:
package com.demo5;
public class Test {
public static void main(String[] args) {
try {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[0]);
int c = a / b;
} catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie) {
// 捕获多异常时,异常变量默认有final修饰
// 下面代码会报错
// ie = new RuntimeException();
} catch (Exception e) {
// 下面代码不会报错
e = new RuntimeException();
}
}
}
4 异常转译和异常链
对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。
当业务层访问持久层,而持久层出现SQLException异常,被业务层捕获,业务层不应该再把该SQL异常传递给表现层。通常的做法是:先捕获原始异常,然后再抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。
假如需要实现工资计算方法,则程序的代码结构如下:
try {
// 实现结算工资的业务逻辑
....
} catch (SQLException e) {
//把原始异常记录下来,留给管理员
....
//新建异常,message就是对用户的提示
throw new RuntimeException("访问底层数据库时出现异常。");
}catch (Exception e) {
//把原始异常记录下来,留给管理员
....
//新建异常,message就是对用户的提示
throw new RuntimeException("系统出现未知异常。");
}
在JDK 1.4以前,程序员必须自己编写代码来保持原始异常信息。从JDK 1.4以后,所有Throwable的子类在构造器中都可以接收一个cause对象作为参数。这个cause就是用来表示原始异常,这样就可以把原始异常传递给新的异常,这是一种典型的链式处理,因此被称为“异常链”。
异常链使得即使在当前位置创建并抛出了新的异常,也能通过cause追踪到异常最初发生的位置。上述代码结构可以改写为:
try {
// 实现结算工资的业务逻辑
....
} catch (SQLException e) {
throw new RuntimeException("访问底层数据库时出现异常。",e);
}catch (Exception e) {
throw new RuntimeException("系统出现未知异常。",e);
}
5 异常跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。
示例:
package com.demo5;
public class Test {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
thirdMethod();
}
public static void thirdMethod() {
throw new RuntimeException("自定义异常");
}
}
运行结果:
Exception in thread "main" java.lang.RuntimeException: 自定义异常
at com.demo5.Test.thirdMethod(Test.java:18)
at com.demo5.Test.secondMethod(Test.java:14)
at com.demo5.Test.firstMethod(Test.java:10)
at com.demo5.Test.main(Test.java:6)
异常从thirdMethod开始触发,传到secondMethod,传到firstMethod,最后传到main方法,这个过程就是Java的异常跟踪栈。面向对象的应用程序运行时,会发生一系列的方法调用,从而形成“方法调用栈”,异常的传播方向与“方法调用栈”方向相反。
6 自定义异常
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException。
7 异常最佳实践
7.1 区分异常和普通错误
这种错误情况是:把异常和普通错误处理混淆在一起,不再编写任何错误处理代码,而是简单地抛出异常来代替所有的错误处理。
对于已知的、完全确定的普通错误,应该编写处理这种错误的代码,增加程序的健壮性;对于外部的,不能确定预知的运行时错误才使用异常。例如,下棋游戏中,处理“用户输入坐标点已经有棋子”的方式有:
方式1(正常的业务逻辑处理):
if (!gb.board[xPos - 1][yPos - 1].equals("+")) {
System.out.println("您输入的坐标点已有棋子了,请重新输入");
continue;
}
方式2(使用异常代替处理):
if (!gb.board[xPos - 1][yPos - 1].equals("+")) {
throw new Exception("您试图下棋的坐标点已经有棋子了");
}
方式2没有提供有效的处理代码,虽然简单,但是Java运行时接收到这个异常后,还需要进入相应的catch块来捕获该异常,所以运行效率差些。
总之,异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离。对于一些完全可知,而且处理方式清除的错误,程序应该提供相应的业务逻辑判断和错误处理代码,而不是将其笼统地称为异常。
7.2 异常和正常的流程控制
异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的流程控制。
7.3 不要使用过于庞大的try块
当try块太庞大时,try块中出现异常的可能性大大增加,分析异常原因困难,try后面紧跟大量的catch块,增加了编程复杂度。正确做法是把大块try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
7.4 避免使用catch all语句
catch all是指可以捕获所有异常的catch块。例如:
try {
} catch (Throwable e) {
}
缺点主要是:所有异常采用相同处理方式,不能分情况处理;压制了异常,可能将程序中的关键异常“悄悄地”忽略。
7.5 不要忽略捕获到的异常
既然已经捕获到异常,那么catch块理应做些有用的事情:处理并修复这个错误。catch块整个为空,或者仅仅打印出错信息都是不妥当的。如果catch块为空,那么程序出错,所有人都看不到任何错误信息。
建议采取如下措施:
处理异常。对于checked异常,尽量修复。
重新抛出新异常。把当前环境下的事尽可能做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出异常,上层调用者来负责处理该异常。