️This article has been over 2 years since the last update.
本文首先讲了如何对null等场景写防御代码,接着介绍了多种返回异常数据的表示方法。
1. 如何写防御代码
在调用第三方数据(这里包括调用其它接口,或者客户端发来的请求)时,一般有三种可能:
- 第一是通过文档白纸黑字进行约束数据源一定不为空,或者到底谁负责校验,因此作为绝对信任返回值,可以不做空判断;
- 第二种就是未知的,可能为null。这种在没有文档、没有源码,甚至接口都是动态代理桩的情况下,只能对代码表示WTF了。
- 第三种返回的是空白的List。
通过上面三种,常见的数据源大多按照如下设计
1 | 1. ["1111","2222","3333"] |
其中1, 2, 3, 4, 5都是常见的API,其中1, 2可以直接进行业务操作,而3, 4, 5需要一个状态码检查。上述API只要JSON反序列化过程是正常的,基本没有问题,不用做空判断可以直接执行业务;但是第五个却返回了null,那么问题来了。
1.1. 对于NULL的处理方法
null是一个很让人厌恶的类型,曾经造成了“10亿美元的错误”,虽然JDK8提供了Optional的设计,然而在目前场景中,很少有Optional的实现。null同时也经常意味不明,有时代表着没找到数据;有时代表着没有错误,甚至有时代表错误码(就像go语言中ret,err = func()
的写法),那么到底如何处理null呢?
1.1.1. 兼容场景
如果你希望直接适配第三方的返回值,可以将null转为一个空白对象,比如Collections.<T>emptyList()
,这样在之后的业务代码中,无论是遍历还是其它转换,因为它本身长度为0,不会产生任何额外错误。
1 | //list的结果不明朗 |
没有找到
不等于null
,也不等于错误
,上面的接口返回null,意义不明,导致出现无谓的防御代码如果是容器类,可以将null装换为Collections.
emptyList() 如果是对象类,请接着看。
上面业务可以使用Common Collection4, RxJava, JDK8 Stream等工具进行处理,不必自己造轮子。
1.1.2. 强硬场景
如果你明确不接受null,你可以直接抛出异常,进行甩锅,值得注意的是,这里仅仅是对开发者甩锅,而不能对用户甩锅。
1 | List<String> list = getService().queryById("test"); |
1.2. 对于异常/错误的处理方法
当在调用接口等不安全的场景中,可能因为网络连接等问题而抛出异常,因此,所有心里没有底的接口都必须放在try/catch中
常见异常/错误的处理设计如下
错误码风格的设计(比较成熟的使用广泛的设计):
1 | try{ |
返回空白数据的设计(如果打了Log,此方法也比较可行):
1 | try{ |
返回null的设计(这个是大坑,所有人都没意见吧):
1 | try{ |
抛出异常的设计:
1 | try{ |
我的建议如下
- 如果你当前实现的业务代码可以被底层平台全局try/catch保证,那么放心地直接抛出异常吧,这样代码写的最简洁
- 当你面向用户,异常中断了程序的执行,直接返回错误码即可,body可以写null,也可以写
[]
,但是我强烈推荐返回空白列表。 - 当你面向用户,异常可以被处理并接着执行程序,你只需要把它catch住即可。
- 当你面相开发者,异常中断了程序执行,直接抛出。
- 当你面相开发者,异常没有中断了程序执行,打Log跳过。
- 当没有业务错误时,直接返回正确的结果。
2. 如何安全迭代
在获取到数据源后,并不代表里面都不是空的,forEach或者迭代器不能帮你过滤null,例子如下
1 | List<Integer> list = Arrays.asList(1, null, 3, 4, 5); |
你需要这样写
1 | //Java8的方法 |
最好的实践是,你应该记住无论何时都不要把null放入容器(set, map…)中,这是何等怠惰的做法。
3. 如何返回数据
3.1. 如何返回非空的对象
下面是最常见的场景,直接返回了null,不过我认为是一种反模式,推荐加入@Nullable
注解让IDE帮你进行更多的静态分析
1 | //bad practice: "return null" == "no found" |
如果需要更优秀的代码,它是这样的,这样你获取到的值永远都是非空的,我最推荐的是这个方法,它在错误码场景中非常地常见。
1 |
|
还有一种是异常的表示方法,也是符合OOP的
1 | public Employee getByName(String name) { |
3.2. 如何返回List
返回的List永远不要设计为null,否则看起来很丑,而且客户端需要再次校验,推荐如下的形式,这个在上文也讲过了
1 |
|
3.3. SUM
说了这么多,总的就是两句:
- 在面向用户的业务代码中,别人的代码不要信任,一定要过滤掉null,阻止null继续传递;
- 自己的业务代码要对用户/其它开发者负责,一定不要返回null,万不得已时也要加上
@Nullable
标记 - 在面向开发者的底层代码中,直接抛异常,节省联调时间。
4. try/catch过多是否影响性能?
有时候在某些场景,对自己的代码不够自信,对底层不了解,而且是面向用户开发,因此希望处理所有的异常。
这时全局try/catch就登场了,关于全局try/catch,比如RxJava就是这样写的,网上有写评论说会影响性能,但是通过老外的讨论,只有发生了异常才会去进行查询异常表、导出trace,进而影响性能。而如果没有出现异常,是不会发生性能变低的情景的。
我的建议如下:
- 它处理了所有的异常,起码对用户来说不会出现错误堆栈信息。
- 它并不能代替你完成所有的异常处理,在被全局try/catch包裹的更小代码片段中,仍然需要try/catch进行捕获处理。
5. Refference
- http://stackoverflow.com/questions/6546875/collections-emptylist-instead-of-null-check
- https://blogs.msdn.microsoft.com/ericlippert/2009/05/14/null-is-not-empty/
- http://stackoverflow.com/questions/1274792/is-returning-null-bad-design
- http://www.yegor256.com/2014/05/13/why-null-is-bad.html
- Null References, The Billion Dollar Mistake
- 在v2ex上的讨论帖
- Java 异常处理的误区和经验总结