java中的异常处理是大部分程序员积攒多年的痛点,本章主要涉及如何定义、捕获、处理异常事件,如何以合理的日志结构保存出错现场信息,以便快速定位问题。开始浅析之前老四先来带领大家再来梳理一遍java异常的基础知识。
异常分两种大的异常类型,运行异常和受检查异常,这两种异常的区别:
- 运行异常的特点是Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用try…catch语句捕获它,也没有用throws字句声明抛出它,还是会编译通过。
- 除了运行异常外,其他异常都属于受检查异常,这种异常的特点是要么用try…catch捕获处理,要么用throws语句声明抛出,否则编译不会通过。
面试中经常被问到的: 你经常碰到的或者你碰到的异常,举几个例子?
常见运行时异常:
- 空指针异常( java.lang.NullPointerException) –调用了未经初始化的对象或者是不存在的对象
- 类找不到异常( java.lang.ClassNotFoundException) –名称或路径错误
- 数组越界(java.lang.ArrayIndexOutOfBoundException) –下标越界
- 不合法的参数异常( java.lang.IllegalArgumentException) –参数不正确
- 没有访问权限( java.lang.IllegalAccesException) –没有调用指定类的权限
捕获异常代码格式:
1 2 3 4 5 6 7 |
try{ //执行的代码,其中可能有异常。一旦发现异常,则立即跳到catch执行。否则不会执行catch里面的内容 }catch{ //除非try里面执行代码发生了异常,否则这里的代码不会执行 }finally{ //不管什么情况都会执行,包括try catch 里面用了return ,可以理解为只要执行了try或者catch,就一定会执行 finally } |
自定义异常:
1 2 3 4 5 |
class 异常类名 extends Exception{ public 异常类名(String msg){ super(msg); } } |
-
final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义成final类型。关于final的更多了解您可以参考写的这篇《Java面向对象之final修饰符》
-
finally是异常处理语句结构的一部分,表示总是执行。
-
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。JVM不保证此方法总被调用。
-
可以是可被控制的或不可控制的
-
程序员导致的错误
-
应用程序级被处理
error:
-
总是不可控
-
系统错误或者底层资源的错误
-
系统级捕捉
关于try-catch-finally语句块的一些逻辑说明:
-
try块中没有抛出异常,try、catch和finally块中都有return语句(返回值是finally中的return返回的)
- try块中抛出异常,try、catch和finally中都有return语句(返回值是finally中的return返回的)
-
try块中没有抛出异常,仅try和catch中有return语句(返回值是try中的return返回的)
- try、catch中都出现异常,在finally中有返回(返回值是try中的return返回的)
-
try块中抛出异常,try和catch中都有return语句(返回的catch中return值)
-
只在函数最后出现return语句(该返回啥返回啥)
干脆点说就是:只要finally中如果存在return了,那其余的就都不好使了。所以return尽量放在try里面或者函数最后面,尽量不要放在finally中,下面的规约中也提到这件事了。我们接下来开始浅析异常规约。
1.[强制] Java类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch的方式来处理,比如: NullPointerException,IndexOutOfBoundsException等等。
说明: 无法通过预检查的异常除外,比如,在解析字符串形式的数字时,不得不通过catch NumberFormatException来实现。
正例: if (obj != null) {…}
反例: try { obj.method() } catch (NullPointerException e) {…}
老四附言:
说白了就是运行时的异常都是我们码农…不对,软件工程师可以通过编码来避免的,况且如果真的发生这种异常系统也会抛出来,不要把这些异常让try catch来处理,得不偿失了。但是我们切记自己编码逻辑性的严谨,尽量少犯错,尤其像这种空指针的异常估计是很多程序员的痛点吧,职业生涯老四敢打赌肯定遇到过平均不止95.27次。。。
2.[强制] 异常不要用来做流程控制,条件控制。
说明: 异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
老四附言:
老四就经常犯这种错误,经常在try里面写理论上正确的逻辑,在catch中处理错误的业务逻辑,其实潜意识里面我是知道这种方式是不好的,现在慢慢的把这种习惯改过来了,希望看到此书的同学们,我们共勉吧。
3.[强制] catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
说明: 对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例: 用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
老四附言:
对不起我的公司,老四到现在都挺不负责任的,且行且改之吧,抛开公司层面不讲,这是对自己负责。
4.[强制] 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
老四附言:
在对异常进行处理的时候,仅通过print语句是无法对异常的信息做出充分的描述的。所以为了显示更好更具体的细节,Throwable类提供了一些有用的方法来帮助程序员处理,无论是哪一类异常,只要是Throwable的子类都可以使用这些方法来获得更为详细的信息。
- String getMessage() 获取异常的详细信息
- Sting getLocallizedMessage() 获取用本地语言描述的详细信息
- Sting toString() 返回对异常的一个简短的描述
- void printStackTrace() 打印出异常和他调用栈信息到标准的错误流中
- getClass() 返回一个表示这个对象属于哪种类型的对象
5.[强制] 有try块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
老四附言:
引用P3C中的示例供你们参考一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
/** * @author caikang * @date 2017/04/07 */ @Service @Transactional(rollbackFor = Exception.class) public class UserServiceImpl implements UserService { @Override public void save(User user) { //some code //db operation } } /** * @author caikang * @date 2017/04/07 */ @Service public class UserServiceImpl implements UserService { @Override @Transactional(rollbackFor = Exception.class) public void save(User user) { //some code //db operation } } /** * @author caikang * @date 2017/04/07 */ @Service public class UserServiceImpl implements UserService { @Autowired private DataSourceTransactionManager transactionManager; @Override @Transactional public void save(User user) { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can only be done programmatically def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); try { // execute your business logic here //db operation } catch (Exception ex) { transactionManager.rollback(status); throw ex; } } } |
另外简单介绍一下P3C,他是我们正在浅析的Java代码规约扫描ide插件,支持idea和eclipse,根据本书让你规范编码,P3C的GitHub地址请戳这个链接-alibaba/p3c。
6.[强制] finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。
说明: 如果JDK7及以上,可以使用try-with-resources方式。
老四附言:
简单介绍一下这个try-with-resource,他是在jdk7中才增加的语法,其实也是实现了一个语法糖,并不是在JVM中新增的功能,关于语法糖您可以参考一下语法糖-维基百科。
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
举例来说,许多程序语言提供专门的语法来对数组中的元素进行引用和更新。从理论上来讲,一个数组元素的引用涉及到两个参数:数组和下标向量,比如这样的表达式,get_array(Array, vector(i, j))。然而,许多语言支持这样直接引用 Array[i, j]。同理,数组元素的更新涉及到三个参数,set_array(Array, vector(i, j), value),但是很多语言提供这样直接赋值,Array[i, j] = value。
我们传统的关闭资源的方式,拿输入流来举例好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package com.glorze.junit; import java.io.File; import java.io.FileInputStream; import java.io.IOException; /** * 文件输入流测试 * @ClassName InputstreamTest * @author: glorze.com * @since: 2018年7月11日 下午10:47:12 */ public class InputstreamTest { public static void main(String[] args) { FileInputStream inputStream = null; try { inputStream = new FileInputStream(new File("/home/glorze/temp/glorze.txt")); System.out.println(inputStream.read()); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } |
以上代码应该是我再熟悉不过的文件输入流处理经典代码了,我们需要在finally中处理外部资源的关闭。现在看起来比较麻烦,或者说可读性也比较差。鉴于其他语言都有了自动关闭外部资源的特性语法,于是在jdk7中我们也可以通过try-catch-resource的语法结构来简写以上代码,当然这里的外部资源对象需要实现AutoCloseable接口。改下代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.glorze.junit; import java.io.File; import java.io.FileInputStream; import java.io.IOException; /** * 输入流资源处理的try-catch-resource形式 * @ClassName InputstreamTestWithResource * @author: glorze.com * @since: 2018年7月11日 下午10:56:26 */ public class InputstreamTestWithResource { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream(new File("/home/glorze/temp/glorze.txt"))) { System.out.println(inputStream.read()); } catch (IOException e) { e.printStackTrace(); } } } |
是不是看起来简洁多了,也不需要我们来处理资源关闭了,只需要我们在try块中完成外部资源句柄对象的创建,java就会确保资源关闭的自动执行,但是老四也说了,毕竟这只是实现了一个语法糖,上面代码反编译之后其实跟我们最早的笨方式如出一辙,只不过更有利于我们程序开发人员更好的编码,消除样板式代码。关于Java反编译可以参考老四的《你可能不知道的两个逆天的Java反编译在线网站 墙裂推荐》、《介绍几个著名的实用的Java反编译工具,提供下载》这两篇文章。
7.[强制] 不要在finally块中使用return。
说明: finally块中的return返回后方法结束执行,不会再执行try块中的return语句。
老四附言:
更多内容可以参考一下文章开头的基础知识部分。
8.[强制] 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明: 如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
老四附言:
输入输出流异常的捕获代码你写个NumberFormatException那不是扯淡呢吗?
9.[推荐] 方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。
说明: 本手册明确防止NPE(NullPointerException)是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。
老四附言:
所以该判空判空,别怪人家给你返回空你说你自己没问题好伐?
10.[推荐] 防止NPE(NullPointerException),是程序员的基本修养,注意NPE产生的场景:
- 返回类型为基本数据类型, return包装数据类型的对象时,自动拆箱有可能产生NPE。反例: public int f() { return Integer 对象},如果为null,自动拆箱抛NPE。
- 数据库的查询结果可能为null。
- 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。
- 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
- 对于Session中获取的数据,建议NPE检查,避免空指针。
- 级联调用obj.getA().getB().getC(); 一连串调用,易产生NPE。
正例: 使用JDK8的Optional类来防止NPE问题。
老四附言:
首先我们再来复习一下Java的自动拆箱和自动装箱。
我们都知道Java语言虽说是面向对象的语言,但是为了照顾程序员的传统习惯,它也包含了8中基本数据类型。所以,为了能让这8中基本数据类型能更好的的游走在Java的海洋当中,他们做了8个对应的包装类,具体哪8个包装类我也懒得说了。然后在JDK1.5之后为了解决基本类型变量和包装类对象之间的转换,他们搞了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。
所谓自动装箱,就是可以把一个基本类型变量直接赋值给对应的包装类变量,或者赋值给Object变量;自动拆箱则相反.允许直接把包装类对象直接赋值给一个对应的基本类型变量。这种方式大大简化了基本类型变量和包装类对象之间的转换过程,但是我们在使用的过程要注意类型匹配,别整个int变量去装箱成Boolean类去就尴尬了。
看一个简单的代码实例深化一下印象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.glorze.oop; /** * Java中的自装箱/拆箱 * @ClassName AutoBoxingUnboxing * @author: glorze.com * @since: 2018年7月12日 下午10:59:35 */ public class AutoBoxingUnboxing { public static void main(String[] args) { Integer glorze = 5; int num = glorze; boolean flag = true; Boolean b = flag; System.out.println(num); System.out.println(b); } } |
一道经典的Java笔试题,我们再来回味一下: 请看如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.glorze.oop; /** * Java中的自装箱/拆箱 * @ClassName AutoBoxingUnboxing * @author: glorze.com * @since: 2018年7月12日 下午10:59:35 */ public class AutoBoxingUnboxing { public static void main(String[] args) { Integer ia = 88; Integer ib = 88; Integer ic = 200; Integer id = 200; System.out.println("两个88自动装箱后是否相等: " + (ia==ib)); System.out.println("两个200自动装箱后是否相等: " + (ic==id)); } } |
初学者肯定是费解为什么两个结果不一样了,其实这个就与Integer的类设计有关系了。我们贴出Integer的源码来看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } |
从上面可以看出,系统把一个-128~127之间的整数自动装箱,并放入到cache中缓存起来了,然而以外的整数系统就会总是重新创建一个Integer实例了。此外,再说一点,Java8再次增强了包装类无符号的算术运算,例如为Integer、Long增加了toUnsignedString、parseUnsignedXxx等方法,我不多磨叽了。
接下来我们再谈谈这个JDK8的Optional类,Optional是java 8中的新特性,我们做码农的被NPE问题折磨了这么多年的确蛮痛苦的,可能是因为感动了上帝吧。简单描述一下关于Optional的用法。我们之前的NPE检查代码可能是这么写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package com.glorze.optional; /** * 学生类 * @ClassName Student * @author: glorze.com * @since: 2018年7月12日 下午1:27:57 */ public class Student { private String name; public Student() { super(); } public Student(String name) { super(); this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student [name=" + name + "]"; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package com.glorze.optional; /** * 年级类 * @ClassName Grade * @author: glorze.com * @since: 2018年7月12日 下午1:29:45 */ public class Grade { private Student student; public Grade() { super(); } public Grade(Student student) { super(); this.student = student; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override public String toString() { return "Grade [student=" + student + "]"; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package com.glorze.optional; public class School { private Grade grade; public School() { super(); } public School(Grade grade) { super(); this.grade = grade; } public Grade getGrade() { return grade; } public void setGrade(Grade grade) { this.grade = grade; } @Override public String toString() { return "School [grade=" + grade + "]"; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.glorze.optional; /** * 空指针测试 * @ClassName NpeTest * @author: glorze.com * @since: 2018年7月12日 下午1:35:11 */ public class NpeTest { public String testNpe(School school) { String name = ""; if(null != school) { Grade grade = school.getGrade(); if(null != grade) { Student student = grade.getStudent(); if(null != student) { // 已经足够说明问题,所以name的判空老四没敲 name = student.getName(); } } } return name; } } |
然后我们通过Optional可以避免这种繁琐的写法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.glorze.optional; import java.util.Optional; /** * 空指针测试 * @ClassName NpeTest * @author: glorze.com * @since: 2018年7月12日 下午1:35:11 */ public class NpeOptionalTest { public String testNpe(School school) { return Optional.ofNullable(school) .map(s->s.getGrade()) .map(g->g.getStudent()) .map(stu->stu.getName()) .orElse(""); } } |
如你所见,Optional给了我们一个真正优雅的Java风格的方法来解决null安全问题。其中map方法输入参数是lambda表达式,自动封装每个函数的返回值,而且支持这种链式写法,代码看起来直接就变得舒服起来。Optional类其实是一个包装非空对象的容器类型。使用缺省值表示null值,意图为我们在java系统中减少空指针异常。具体更多的关于Java8中Optional类的介绍可以参考官方文档(自备谷歌翻译)。
11.[推荐] 定义时区分unchecked/checked异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如: DAOException/ServiceException等。
老四附言:
如何自定义异常基础知识里面已经给出一个简单的实例了。
12.[推荐] 对于公司外的http/api开放接口必须使用”错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装 isSuccess()方法 、”错误码”、”错误简短信息”。
说明: 关于RPC方法返回方式使用Result方式的理由:
- 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
- 如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
老四附言:
经验!!!
13.[参考] 避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。
说明: 随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例: 一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {…}
老四附言:
面向对象的三个基本特征。。。
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。抱拳。