• 【从零单排】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不能滥用,能用其它方法达到相同效果最好。

    参考

  • 相关阅读:
    Windows Phone 7 Coding4Fun的弹出框来源:http://www.cnblogs.com/majian714/archive/2011/12/02/2272060.html
    jsp连接mysql的增删改操作
    各种数据库的比较
    jsp连接mysqlupdata操作
    jsp连接Mysql关键代码
    Duwamish学习笔记
    值类型和引用类型的区别
    Factory Method模式的学习
    实现事务的几种方法
    提高Dotnet应用程序性能的技巧
  • 原文地址:https://www.cnblogs.com/maxstack/p/13094960.html
Copyright © 2020-2023  润新知