关于异常Exception,相信大家在开发中都多多少少遇到过,也应该知道要Catch住Exception。本文从实战出发,从头再把这个知识点梳理下。
概述
Exception
和Error
都继承自Throwable
。结构如下:
Throwable
Error
VirtualMachineError
OutOfMemoryError
StackOverflowError
Exception
IOException
SQLException
XMLParseException
RuntimeException
ArithmeticException
ClassCastException
IndexOutOfBoundsException
NullPointerException
Error和Exception,都是指程序遇到了问题。区别主要在于,一是遇到的是什么样的问题,二是该如何处理。
Error
是错误,指程序运行时遇到的无法处理的错误,多数情况与代码无关,可能是JVM层面的问题,比如OutOfMemoryError
等等。遇到这类问题,程序一般必须停止。Exception
是异常,指程序运行时发生的一些超出预期的异常情况,有些异常可以人为预见,比如IOException
,有些无法预见,比如IndexOutOfBoundsException
。一般来说,我们会尽量尝试处理异常,使得程序能顺利地运行下去。
这里有个问题:为什么Error和Exception都继承自父类Throwable?Exception可以抛出,让上层去统一处理,这个可以理解。但是Error为什么要抛出呢?不是应该直接把程序挂掉吗?
笔者答:看了下Throwable
实现的方法,如下图所示,比如getMessage()
,getStackTrace()
等等。然后再分别看了下Error
和Exception
的方法,发现都是继承自父类的。所以一个原因是实现类的复用。
可查异常和不可查异常
定义与处理
Checked Exception
,指可查异常。必须在代码中显示地进行处理,即catch或者throw。否则编译会报错。Unchecked Exception
,指不可查异常。不强制检查,尽量通过程序员的经验避免。
有一个快速记忆的方法,除去Error不讨论(有的地方把Error也算作Unchecked Exception,但笔者认为这样分类容易引起混淆,没什么必要),
Unchecked Exception
就是RuntimeException
。而与之对应的,不是RuntimeException
的其它所有Exception
,都是Checked Exception
。
Java文档里是这样写的
The class Exception and any subclasses that are not also subclasses of RuntimeException are checked exceptions.
Checked exceptions need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.
-- https://docs.oracle.com/javase/7/docs/api/java/lang/Exception.html
举例 Checked Exception - IOException
文件读写时,可能会发生IO异常,这里我们做了抛出处理。
public static void main(String args[]) throws IOException {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("input.txt");
out = new FileOutputStream("output.txt");
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
}finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
举例 Unchecked Exception - IndexOutOfBoundsException
使用Arraylist里的值时发生NPE
。
这个list里的元素的值,是运行时添加/修改的,编程时无法知道会不会出错。也就没有办法抛出或者抓住异常。
public static void main(String args[]) {
List list = new ArrayList();
list.add("abc");
list.clear();
System.out.println(list.get(0));
}
结果
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.get(ArrayList.java:433)
at p4.TestException.main(TestException.java:11)
我们能做的是在使用这个list时检查边界,保证不会NPE
。代码修改如下
public static void main(String args[]) {
List list = new ArrayList();
list.add("abc");
list.clear();
if (list!=null && !list.isEmpty()) {
System.out.println(list.get(0));
} else {
System.out.println("list EMPTY");
}
}
小结
其实,上述例子中,理论上,ArrayList
可以在定义get
方法的时候,抛出一个NPE
,强制程序员们每次使用时要对异常进行处理。但是这样开销很大,用Catch Exception方法达到检查边界的效果,实在是高射炮打蚊子。
笔者理解,Checked Exception
和Unchecked Exception
,是语言编写者们从宏观上综合考虑后,对异常进行的一个分类。大家经常跳的坑,就放到Checked Exception
中,提醒你这里需要注意。那些比较显而易见的错误,就放到Unchecked Exception
中。
实例1 - 在哪里try catch很重要
接下来,我们看几个实际的例子,加深理解。
原先的操作是getFromDB_1
-> getFromDB_2
。
后来加了一个操作,getFromDB_1A
,这个操作,有的时候会抛出RuntimeException
。
由于try catch
在最外层,再加上没有打印异常信息,这个bug隐藏了很久。
不仔细分析log,很难发现原来getFromDB_2
被跳过了。
public static void main(String args[]) {
try {
System.out.println("Process A start...");
System.out.println(getFromDB_1());
System.out.println(getFromDB_1A());
System.out.println(getFromDB_2());
} catch (Exception e) {
//System.out.println(e.getMessage());
}
System.out.println("Process B start ...");
}
private static int getFromDB_1() {
return 1;
}
private static int getFromDB_1A() {
throw new RuntimeException("no value from getFromDB_2");
}
private static int getFromDB_2() {
return 2;
}
结果
Process A start...
1
Process B start ...
上述的写法,其实还是比较容易发现问题的,这个getFromDB_1A
方法明显不太对啊。这里其实将问题做了简化处理,实际的代码大概长这样,很具有迷惑性:
private static int getFromDB_1() {
return jdbctempalte.excecute("SQL_1");
}
private static int getFromDB_1A() {
return jdbctempalte.excecute("SQL_1A");
}
private static int getFromDB_2() {
return jdbctempalte.excecute("SQL_2");
}
分析,getFromDB_1A
方法应该是照抄getFromDB_1
和getFromDB_2
的,估计当时的程序员想法是这样的:这几个方法使用情况类似,我copy一下应该不会有问题吧?
不加思考地copy肯定不对,换一个角度思考,原来的代码就没有问题吗?可能真的只是原来运气好,没有遇到抛异常的情况。
实际上,我们应该把try catch
放到DB查询的地方,而不是上层。如下:
private static int getFromDB() {
try {
return jdbctempalte.excecute("SQL");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
这样,各个getFromDB
方法之间就不会互相影响了。
实例1.5 - jdbctemplate
关于上面的例子,再延展一点讲讲jdbctemplate
。当你在代码里敲下jdbctemplate.excecute
IDE却没有报错的时候,心里有没有一丝丝疑问?
DB query的操作难道不是应该抛Checked Exception
- SQLException
吗?为啥这里没有throw
,没有catch
,也能编译通过呢?
去查看Spring Jdbc源码,发现原来这里是catch住了Checked Exception
- SQLException
,然后抛了一个Unchecked Exception
- DataAccessException
啊!
@Override
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("StatementCallback", sql, ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
DataAccessException
是Spring自己创建的类,继承自RuntimeException
Spring为啥好用呢?当你写jdbctempalte.excecute
时,不需要用难看的try catch
包裹,多么优雅,简洁。
(当然,我们要记得,用到的时候想想看会不会出exception,需不需要handle)
实例2 - 不能滥用try catch
当然,try catch
也是有开销的,不能滥用。
比如,从文件中读取一些信息,然后将某一列转成Double
类型的值,然后使用。
这里,我们分析文件,知道有些列是正常的数字,有些列是乱码。
当parseDouble
时,我们当然可以用NumberFormatException
去catch住,但是,有没有更好的办法呢?
其实,我们可以直接用NumberUtils.isNumber
先判断一下。
import org.apache.commons.lang3.math.NumberUtils;
public static void main(String[] args) {
// case 1: normal
long start_1 = System.currentTimeMillis();
String input_1 = "10.54";
double double_1 = Double.parseDouble(input_1);
String timeElapsed_1 = DurationFormatUtils.formatPeriod(start_1, System.currentTimeMillis(), "ss.SSS");
System.out.println(double_1 + ", time: " + timeElapsed_1);
// case 2: catch exception
long start_2 = System.currentTimeMillis();
String input_2 = "x2sdf";
double double_2 = 0;
for (int i=0; i<10000000; i++) {
try{
double_2 = Double.parseDouble(input_2);
} catch(NumberFormatException e) {
double_2 = -1;
}
}
String timeElapsed_2 = DurationFormatUtils.formatPeriod(start_2, System.currentTimeMillis(), "ss.SSS");
System.out.println(double_2 + ", time: " + timeElapsed_2);
// case 3: check input
long start_3 = System.currentTimeMillis();
String input_3 = "x2sdf";
double double_3 = 0;
for (int i=0; i<10000000; i++) {
double_3 = NumberUtils.isNumber(input_3)? Double.parseDouble(input_3) : -1;
}
String timeElapsed_3 = DurationFormatUtils.formatPeriod(start_3, System.currentTimeMillis(), "ss.SSS");
System.out.println(double_3 + ", time: " + timeElapsed_3);
}
结果
10.54, time: 00.000
-1.0, time: 05.768
-1.0, time: 00.080
可以看到,运行1000万次,结果差别还是蛮大的。用catch exception
耗时5秒多,用NumberUtils.isNumber
耗时0.08秒。
所以,能不用异常,且也能达到相同效果的话,尽量不要用。
总结
Exception
和Error
都继承自Throwable
。Unchecked Exception
约等于RuntimeException
。Checked Exception
约等于其它的Exception
,需要throw
或catch
。try catch
写在哪里很重要,不一定写在最外层就是最优的。try catch
不能滥用,能用其它方法达到相同效果最好。