今天 review 同事的代码,发现了如下代码片段
// 这是一段伪代码
public SomeObject func() {
...
if (发生异常) {
log.error(...);
return null;
}
...
return 正常值;
}
类似的代码我见过很多,自己曾经也写过这样的代码。
但是如此的写法实际并不安全,因为调用方很容易取到一个 null 值,进而诱发空指针问题。
为了重构这个代码,有如下几种处理策略
☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️
☀️ 1. 返回 Optional 对象 ☀️
☀️ 2. 返回包装类 ☀️
☀️ 3. 异常时返回空对象或默认值 ☀️
☀️ 4. 上抛异常 ☀️
☀️ 5. 必须要返回 null 时的小技巧 ☀️
☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️ ☀️ ☀️
在文章的最后,我会给出另外一种 常见但是不推荐 的解决方案:通过异常的返回值标记内部的状态
| 方案 | 额外代码量 | 灵活度 | 调用者额外判定 | 错误细节感知 | 沉默的错误 |
|---|---|---|---|---|---|
| 返回 Optional 对象 | 无 | 一般 | 有 | 不可以 | 可以避免 |
| 返回包装类 | 有,但全局可用 | 灵活 | 有 | 可以 | 可以避免 |
| 异常时返回空对象或默认值 | 有 | 一般 | 无 | 不可以 | 容易发生 |
| 上抛异常 | 有,但不多 | 灵活 | 有,但解耦在 catch 中处理 | 可以 | 可以避免 |
必须要返回 null 时的小技巧 | 无 | 低 | 有 | 不可以 | 可以避免 |
表格中维度的解释:
Optional 对象Optional 对象是 java 特有的,但其他语言也有类似的实现。这种方法的伪代码如下
// 函数改造
public Optional func() {
...
if (发生异常) {
log.error(...);
return Optional.empty();
}
...
return Optional.of(正产值);
}
这样调用方就知道,函数返回值是可能为空的,可以通过函数式编程的方式进行接下来的处理。
// 调用方代码
Optional result = func();
result.ifPresent(obj -> {...});
这种方式和 Optional 类似,只不过包装类是自己写的。优点是大大增加了灵活性
Optional 对象的时候发现提供的 api 不一定能满足需求Optional 就显得无能为力了(虽然这违反了范式,但有时候真的很便捷)例如可以使用一个全局的函数返回包装类
// 包装类
public class FunctionResult {
private boolean success;
private String msg;
private ErrorCodeEnum code;
private T data;
// 这里可以提供丰富灵活的 api
// 例如 void ifSuccess(Consumer consumer);
// 还可以 FunctionResult ifSuccess(Consumer consumer) 用来提供链式调用
// 其他的也可以大胆地进行设计
// 当然最简单的,就是让调用方判定 success 的值
}
// 函数改造
public FunctionResult func() {
...
if (发生异常) {
log.error(...);
return FunctionResult.failure("错误信息"); // 可以提供丰富的构造函数
}
...
return FunctionResult.success(正产值);
}
这样调用方就可以知道返回值有可能是失败的,及时做出检测。
// 调用方代码
FunctionResult result = func();
result.ifSuccess(obj -> {...});
这也是一种常见的设计模式,所谓 “空对象” 也被称作 “null 对象”,例如
// 空对象示例
public class Circle {
/**
* 半径
*/
private int r;
public Circle(int r) {
this.r = r;
}
public int area() {
return pi * r * r;
}
}
// 一个 null 圆,半径为 0
public class NullCircle extends Circle {
public NullCircle() {
super(0);
}
}
这样当发生异常时返回 “空对象” 调用方完全不需要担心空指针的问题。
另外一种常见的思路,是返回一个默认值,很多的包也会采用类似的设计,例如返回字符串的函数,当发生异常时返回空字符串("")作为默认值而不是返回 null;或者在上面圆形的例子中,如果是特定的业务领域,有可能可以返回一个 “标准圆” 作为默认的返回值。
可以在发生异常时抛出合理的异常或者是自定义的异常,伪代码如下
// 函数改造
public Optional func() throws 自定义的异常 {
...
if (发生异常) {
throw 自定义的异常;
}
...
return Optional.of(正产值);
}
// 调用方代码
try {
SomeObject result = func();
} catch(自定义的异常 e) {
// 按需进行异常处理
}
警告:不要一味的抛出 Exception 类而应该自己去继承 Exception 类实现自定义的异常。否则会造成异常处理混乱,代码难以维护。
null 时的小技巧如果觉得上述方案都太过麻烦了,另一种可能是工期过紧,非得想要返回 null 值,那么通过以下两个小技巧,也能在一定程度上缓解这一问题
比如原来你的函数名字是 myFunc,简单的改成 myFuncNullable,调用方就会意识到返回值可能为 null,进而避免空指针。
在你的函数上使用 @Nullable 注解,大多数的现代 IDE 都能检测到这个注解,在编写代码的时候,如果调用方使用其返回值不合理,就会给出 “标黄” 的提示,一眼识别问题。

对于返回数字类型的函数,一种常见的写法如下
// 函数改造
pubic int func() {
...
if (发生异常) {
log.error(...);
return -1;
}
...
return 正产值;
}
这种“错误码”的写法可以追溯到 C 语言时代,C 语言比较偏底层,而且早期内存很小,这种写法很正常。但是时至今日,使用高级语言的我们应该用表现力更加丰富的手段,原因如下:
-1 这种错误码可读性很差,有时候需要深入实现才能理解 -1 是什么含义,-2 又是什么func 返回值含义发生扩展,从正数扩展到负数,改造起来会很麻烦