关于Null、Empty与Exception的思考
2016-09-15 / modified at 2022-05-01 / 2.2k words / 9 mins
️This article has been over 1 years since the last update.

本文首先讲了如何对null等场景写防御代码,接着介绍了多种返回异常数据的表示方法。

1. 如何写防御代码

在调用第三方数据(这里包括调用其它接口,或者客户端发来的请求)时,一般有三种可能:

  • 第一是通过文档白纸黑字进行约束数据源一定不为空,或者到底谁负责校验,因此作为绝对信任返回值,可以不做空判断;
  • 第二种就是未知的,可能为null。这种在没有文档、没有源码,甚至接口都是动态代理桩的情况下,只能对代码表示WTF了。
  • 第三种返回的是空白的List。

通过上面三种,常见的数据源大多按照如下设计

1
2
3
4
5
6
1. ["1111","2222","3333"]
2. []
3. {"code":"0","message":"success","body":{ ["1111","2222","3333"]}}
4. {"code":"1","message":"arg illegal","body":null}
5. {"code":"1","message":"arg illegal","body":[]}
6. null

其中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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//list的结果不明朗
List<String> list = getService().queryById("test");

//先调用`safe()`保证安全,再进行业务
safe(list).stream()
.filter(s -> s.lenght() > 3)
.collect(Collectors.toList());

//这里就是你对null的适配
public static <T> Collection<T> safe(Collection<T> collection){
if(collection==null){
Log.d("input list is null, will return a emptyList");
return Collections.<T>emptyList();
}
return collection;
}

没有找到不等于null,也不等于错误,上面的接口返回null,意义不明,导致出现无谓的防御代码

如果是容器类,可以将null装换为Collections.emptyList()

如果是对象类,请接着看。

上面业务可以使用Common Collection4, RxJava, JDK8 Stream等工具进行处理,不必自己造轮子。

1.1.2. 强硬场景

如果你明确不接受null,你可以直接抛出异常,进行甩锅,值得注意的是,这里仅仅是对开发者甩锅,而不能对用户甩锅。

1
2
3
4
5
List<String> list = getService().queryById("test");
//强行甩锅
Objects.requireNonNull(list);
//开始业务
list.....

1.2. 对于异常/错误的处理方法

当在调用接口等不安全的场景中,可能因为网络连接等问题而抛出异常,因此,所有心里没有底的接口都必须放在try/catch中

常见异常/错误的处理设计如下

错误码风格的设计(比较成熟的使用广泛的设计):

1
2
3
4
5
6
try{
//some Exception
return Msg.of("0","success!",resp);
} catch(ChildException ce){
return Msg.of("1","call {xxService} failed");
}

返回空白数据的设计(如果打了Log,此方法也比较可行):

1
2
3
4
5
6
try{
//some Exception
return Msg.of("0","success!",resp);
} catch(ChildException ce){
return Msg.of("1","failed",Collections.emptyList());
}

返回null的设计(这个是大坑,所有人都没意见吧):

1
2
3
4
5
6
try{
//some Exception
return msg("0","success!",resp);
} catch(ChildException ce){
return null;
}

抛出异常的设计:

1
2
3
4
5
6
try{
//some Exception
return msg("0","success!",resp);
} catch(ChildException ce){
throw ce;
}

我的建议如下

  1. 如果你当前实现的业务代码可以被底层平台全局try/catch保证,那么放心地直接抛出异常吧,这样代码写的最简洁
  2. 当你面向用户,异常中断了程序的执行,直接返回错误码即可,body可以写null,也可以写[],但是我强烈推荐返回空白列表。
  3. 当你面向用户,异常可以被处理并接着执行程序,你只需要把它catch住即可。
  4. 当你面相开发者,异常中断了程序执行,直接抛出。
  5. 当你面相开发者,异常没有中断了程序执行,打Log跳过。
  6. 当没有业务错误时,直接返回正确的结果。

2. 如何安全迭代

在获取到数据源后,并不代表里面都不是空的,forEach或者迭代器不能帮你过滤null,例子如下

1
2
3
4
5
6
List<Integer> list = Arrays.asList(1, null, 3, 4, 5);
//无论使用Java8的forEach,还是迭代器,它们都不保证null过滤
for (Integer integer : list) {
//跑到null时将抛出空指针
System.out.println("integ2er = " + integer.toString());
}

你需要这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Java8的方法
list.stream().filter(i -> i!=null).doSomething;
//RxJava的方法
Obeservable.from(list).filter(i -> i!= null).doSomething();
//自己写JDK8一下的工具类或者用Guava等工具类
static <T> List<T> filter(List<T> list,Predicate<T> p){
List<T> arr = new ArrayList();
for(T t: list){
if(!p.test(t)){
arr.add(t);
}
}
return arr;
}

最好的实践是,你应该记住无论何时都不要把null放入容器(set, map…)中,这是何等怠惰的做法。

3. 如何返回数据

3.1. 如何返回非空的对象

下面是最常见的场景,直接返回了null,不过我认为是一种反模式,推荐加入@Nullable注解让IDE帮你进行更多的静态分析

1
2
3
4
5
6
7
8
9
//bad practice: "return null" == "no found"
@Nullable
public Employee getByName(String name) {
int id = database.find(name);
if (id == 0) {
return null;
}
return new Employee(id);
}

如果需要更优秀的代码,它是这样的,这样你获取到的值永远都是非空的,我最推荐的是这个方法,它在错误码场景中非常地常见。

1
2
3
4
5
6
7
8
@NonNull
public Employee getByName(String name) {
int id = database.find(name);
if (id == 0) {
return Employee.NOBODY;
}
return Employee(id);
}

还有一种是异常的表示方法,也是符合OOP的

1
2
3
4
5
6
7
public Employee getByName(String name) {
int id = database.find(name);
if (id == 0) {
throw new EmployeeNotFoundException(name);
}
return Employee(id);
}

3.2. 如何返回List

返回的List永远不要设计为null,否则看起来很丑,而且客户端需要再次校验,推荐如下的形式,这个在上文也讲过了

1
2
3
4
5
6
7
8
9
@NonNull
public List<Employee> getByAge(String age) {
List<Employee> list = database.find(age);
if (list == null) {
return Collections.<Employee>emptyList();
} else{
return list;
}
}

3.3. SUM

说了这么多,总的就是两句:

  1. 在面向用户的业务代码中,别人的代码不要信任,一定要过滤掉null,阻止null继续传递;
  2. 自己的业务代码要对用户/其它开发者负责,一定不要返回null,万不得已时也要加上@Nullable标记
  3. 在面向开发者的底层代码中,直接抛异常,节省联调时间。

4. try/catch过多是否影响性能?

有时候在某些场景,对自己的代码不够自信,对底层不了解,而且是面向用户开发,因此希望处理所有的异常。

这时全局try/catch就登场了,关于全局try/catch,比如RxJava就是这样写的,网上有写评论说会影响性能,但是通过老外的讨论只有发生了异常才会去进行查询异常表、导出trace,进而影响性能。而如果没有出现异常,是不会发生性能变低的情景的。

我的建议如下:

  • 它处理了所有的异常,起码对用户来说不会出现错误堆栈信息。
  • 它并不能代替你完成所有的异常处理,在被全局try/catch包裹的更小代码片段中,仍然需要try/catch进行捕获处理。

5. Refference

  1. http://stackoverflow.com/questions/6546875/collections-emptylist-instead-of-null-check
  2. https://blogs.msdn.microsoft.com/ericlippert/2009/05/14/null-is-not-empty/
  3. http://stackoverflow.com/questions/1274792/is-returning-null-bad-design
  4. http://www.yegor256.com/2014/05/13/why-null-is-bad.html
  5. Null References, The Billion Dollar Mistake
  6. 在v2ex上的讨论帖
  7. Java 异常处理的误区和经验总结