倚楼听风雨
淡看江湖路

阿里巴巴Java开发规约第二章-异常处理篇

java中的异常处理是大部分程序员积攒多年的痛点,本章主要涉及如何定义、捕获、处理异常事件,如何以合理的日志结构保存出错现场信息,以便快速定位问题。开始浅析之前老四先来带领大家再来梳理一遍java异常的基础知识。

异常分两种大的异常类型,运行异常和受检查异常,这两种异常的区别:

  • 运行异常的特点是Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用try...catch语句捕获它,也没有用throws字句声明抛出它,还是会编译通过。
  • 除了运行异常外,其他异常都属于受检查异常,这种异常的特点是要么用try...catch捕获处理,要么用throws语句声明抛出,否则编译不会通过。

面试中经常被问到的: 你经常碰到的或者你碰到的异常,举几个例子?

常见运行时异常:

  • 空指针异常( java.lang.NullPointerException) --调用了未经初始化的对象或者是不存在的对象
  • 类找不到异常( java.lang.ClassNotFoundException) --名称或路径错误
  • 数组越界(java.lang.ArrayIndexOutOfBoundException) --下标越界
  • 不合法的参数异常( java.lang.IllegalArgumentException) --参数不正确
  • 没有访问权限( java.lang.IllegalAccesException) --没有调用指定类的权限

捕获异常代码格式:

自定义异常:

final、finally、finalize的区别:
  • final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
    内部类要访问局部变量,局部变量必须定义成final类型。关于final的更多了解您可以参考写的这篇《Java面向对象之final修饰符

  • finally是异常处理语句结构的一部分,表示总是执行。

  • finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。JVM不保证此方法总被调用。

error与exception的区别:
首先,都继承自Throwable。

不同之处:
exception:
  • 可以是可被控制的或不可控制的

  • 程序员导致的错误

  • 应用程序级被处理

error:

  • 总是不可控

  • 系统错误或者底层资源的错误

  • 系统级捕捉

java 异常继承关系图:

 

关于try-catch-finally语句块的一些逻辑说明:

  1. try块中没有抛出异常,try、catch和finally块中都有return语句(返回值是finally中的return返回的)

  2. try块中抛出异常,try、catch和finally中都有return语句(返回值是finally中的return返回的)
  3. try块中没有抛出异常,仅try和catch中有return语句(返回值是try中的return返回的)

  4. try、catch中都出现异常,在finally中有返回(返回值是try中的return返回的)
  5. try块中抛出异常,try和catch中都有return语句(返回的catch中return值)

  6. 只在函数最后出现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中的示例供你们参考一下:

另外简单介绍一下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。

我们传统的关闭资源的方式,拿输入流来举例好了:

以上代码应该是我再熟悉不过的文件输入流处理经典代码了,我们需要在finally中处理外部资源的关闭。现在看起来比较麻烦,或者说可读性也比较差。鉴于其他语言都有了自动关闭外部资源的特性语法,于是在jdk7中我们也可以通过try-catch-resource的语法结构来简写以上代码,当然这里的外部资源对象需要实现AutoCloseable接口。改下代码如下:

是不是看起来简洁多了,也不需要我们来处理资源关闭了,只需要我们在try块中完成外部资源句柄对象的创建,java就会确保资源关闭的自动执行,但是老四也说了,毕竟这只是实现了一个语法糖,上面代码反编译之后其实跟我们最早的笨方式如出一辙,只不过更有利于我们程序开发人员更好的编码,消除样板式代码。关于Java反编译可以参考老四的《你可能不知道的两个逆天的Java反编译在线网站 墙裂推荐》、《介绍几个著名的实用的Java反编译工具,提供下载》这两篇文章。

7.[强制] 不要在finally块中使用return。

说明: finally块中的return返回后方法结束执行,不会再执行try块中的return语句。

老四附言:

更多内容可以参考一下文章开头的基础知识部分。

8.[强制] 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。

说明: 如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。

老四附言:
输入输出流异常的捕获代码你写个NumberFormatException那不是扯淡呢吗?

9.[推荐] 方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。

说明: 本手册明确防止NPE(NullPointerException)是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。

老四附言:
所以该判空判空,别怪人家给你返回空你说你自己没问题好伐?

10.[推荐] 防止NPE(NullPointerException),是程序员的基本修养,注意NPE产生的场景:
  1. 返回类型为基本数据类型, return包装数据类型的对象时,自动拆箱有可能产生NPE。反例: public int f() { return Integer 对象},如果为null,自动拆箱抛NPE。
  2. 数据库的查询结果可能为null。
  3. 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。
  4. 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
  5. 对于Session中获取的数据,建议NPE检查,避免空指针。
  6. 级联调用obj.getA().getB().getC(); 一连串调用,易产生NPE。

正例: 使用JDK8的Optional类来防止NPE问题。

老四附言:

首先我们再来复习一下Java的自动拆箱和自动装箱。

我们都知道Java语言虽说是面向对象的语言,但是为了照顾程序员的传统习惯,它也包含了8中基本数据类型。所以,为了能让这8中基本数据类型能更好的的游走在Java的海洋当中,他们做了8个对应的包装类,具体哪8个包装类我也懒得说了。然后在JDK1.5之后为了解决基本类型变量和包装类对象之间的转换,他们搞了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。

所谓自动装箱,就是可以把一个基本类型变量直接赋值给对应的包装类变量,或者赋值给Object变量;自动拆箱则相反.允许直接把包装类对象直接赋值给一个对应的基本类型变量。这种方式大大简化了基本类型变量和包装类对象之间的转换过程,但是我们在使用的过程要注意类型匹配,别整个int变量去装箱成Boolean类去就尴尬了。

看一个简单的代码实例深化一下印象:

一道经典的Java笔试题,我们再来回味一下: 请看如下代码:

初学者肯定是费解为什么两个结果不一样了,其实这个就与Integer的类设计有关系了。我们贴出Integer的源码来看一下:

从上面可以看出,系统把一个-128~127之间的整数自动装箱,并放入到cache中缓存起来了,然而以外的整数系统就会总是重新创建一个Integer实例了。此外,再说一点,Java8再次增强了包装类无符号的算术运算,例如为Integer、Long增加了toUnsignedString、parseUnsignedXxx等方法,我不多磨叽了。

接下来我们再谈谈这个JDK8的Optional类,Optional是java 8中的新特性,我们做码农的被NPE问题折磨了这么多年的确蛮痛苦的,可能是因为感动了上帝吧。简单描述一下关于Optional的用法。我们之前的NPE检查代码可能是这么写的。

然后我们通过Optional可以避免这种繁琐的写法,代码如下:

如你所见,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方式的理由:

  1. 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
  2. 如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。

老四附言:

经验!!!

13.[参考] 避免出现重复的代码(Don't Repeat Yourself),即DRY原则。

说明: 随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例: 一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {...}

老四附言:

面向对象的三个基本特征。。。

更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。抱拳。

赞(12) 给你买杜蕾斯
本站原创文章受自媒体平台原创保护,未经允许不得转载高老四博客 » 阿里巴巴Java开发规约第二章-异常处理篇
分享到: 更多 (0)

开始你的表演 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下老四,鼓励我更好的创作

支付宝扫一扫打赏

微信扫一扫打赏