【从零单排】Exception实战总结1

关于异常Exception,相信大家在开发中都多多少少遇到过,也应该知道要Catch住Exception。本文从实战出发,从头再把这个知识点梳理下。

概述

ExceptionError都继承自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()等等。然后再分别看了下ErrorException的方法,发现都是继承自父类的。所以一个原因是实现类的复用。

exp_1

可查异常和不可查异常

定义与处理

  • 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 ExceptionUnchecked 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_1getFromDB_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.excecuteIDE却没有报错的时候,心里有没有一丝丝疑问?

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

exp_2

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秒。

所以,能不用异常,且也能达到相同效果的话,尽量不要用。

总结

  • ExceptionError都继承自Throwable
  • Unchecked Exception约等于RuntimeException
  • Checked Exception约等于其它的Exception,需要throwcatch
  • try catch写在哪里很重要,不一定写在最外层就是最优的。
  • try catch不能滥用,能用其它方法达到相同效果最好。

参考

posted @ 2020-06-23 18:28  MaxStack  阅读(240)  评论(0编辑  收藏  举报