如何优雅的避免空指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NPESolution {

public void withIf(Person person){
if(person != null){
// ...
}
// ...
}

public void withSpringAssert(Person person){
Assert.isTrue(person != null, "person must be not null.");
// ...
}

public void withOptional(Person person){
Optional<Person> personOptional = Optional.ofNullable(person);
// ...
}

public void withJsr305Annotation(@Nonnull Person person){
Optional<Person> personOptional = Optional.of(person);
// ...
}
}

上述的代码是我在日常用于避免空指针(NPE)的常用方式,很长时间内我都热衷于断言(Assert)这类防御性编程方式,防御性编程可以有效的保证方法的输入条件,并在毫无意义的边界情况能够给出有效的提示,何乐而不为呢?事实上防御性编程也确实是一种非常推荐的方式,并且其在 Spring 源码中随处可见。而 JDK8 的 Optional 是否会是一种更优雅的方式呢?亦或许,另有它人?

if 语句是初学者最常使用的处理空指针的方式,直至今日它也在大多数场景被推荐使用。即使是如此简单的方式,其实也可以略微优化。下面是一个使用 if 语句的例子:

1
2
3
4
5
6
if(person == null){
// ...
} else {
// ...
}
return ...;

在现实业务中我们难以避免地会需要解决分支,if-else 是大多数人常用的方式。但是如果分支内部又产生了分支,我们的代码可读性就会大大的降低,因此这里提到的技巧就是“及时终止”。何谓“及时终止”,简单来说就是通过提前终止代码逻辑来减少嵌套 if-else 的复杂度。优化后的代码如下:

1
2
3
4
5
6
7
if(person == null){
// ...
return ...;
}

// ...
return ...;

既然 if 语句已经能够解决空指针问题,那么为什么 Spring 这类开源项目要使用 Assert 呢?原因在于真实业务场景中,空指针这类的边界条件非常多,并且它很有可能对业务方法的毫无意义,因此使用 Assert 的方式会显得清晰明了,如:

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
// org.springframework.validation.beanvalidation.SpringValidatorAdapter
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {

/**
* Create a new SpringValidatorAdapter for the given JSR-303 Validator.
* @param targetValidator the JSR-303 Validator to wrap
*/
public SpringValidatorAdapter(javax.validation.Validator targetValidator) {
Assert.notNull(targetValidator, "Target Validator must not be null");
this.targetValidator = targetValidator;
}

@Override
public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
Assert.state(this.targetValidator != null, "No target Validator set");
return this.targetValidator.validate(object, groups);
}

@Override
public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) {
Assert.state(this.targetValidator != null, "No target Validator set");
return this.targetValidator.validateProperty(object, propertyName, groups);
}

@Override
public <T> Set<ConstraintViolation<T>> validateValue(
Class<T> beanType, String propertyName, Object value, Class<?>... groups) {

Assert.state(this.targetValidator != null, "No target Validator set");
return this.targetValidator.validateValue(beanType, propertyName, value, groups);
}

...
}

当然 Assert 这类防御性编程方式的缺陷也非常明显,业务逻辑中会存在大量的判空逻辑,通过 Assert 代替 if 语句的方式会使得方法内部存在大量的防御性代码,这并不能提高代码质量,因此 防御性代码常用于输入参数校验。而业务逻辑中的 NPE 解决方案应该是 Optional,构建 Optional 对象的方式通常为 ofNullable 方法或 of 方法,它们的区别在于传入对象是否允许为空

1
2
Optional<Person> personOptional = Optional.ofNullable(person);
Optional<Person> personOptional = Optional.of(person); // null is not allowed

我们可以在 Optional 实现类中找到大量 防御性代码 + Optional.of() 组合使用的应用场景,如:

1
2
3
4
5
6
7
8
// java.util.Optional#filter
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate); // Assert 类似的效果
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}

基于上述的方式,我们可以基本完成一个比较优雅的避免空指针的模式了,并且当我们错误的传入空指针时,编译器(如 idea)会在运行期前及时的提醒我们方法不允许为空。那么这就够了么?还不够。在很多时候,我们会遇到遗留代码或提供三方jar 包,调用方往往会苦于无法确定传入参数是否允许为空,从而不得不研究方法实现。因此更优雅的方式是,我们对外提供的接口(public)可以通过标记注解来对接口进行说明,而此类注解同样能触发编译器的警告。JSR 305 规范已经提供了此类注解,我们只需引入 com.google.code.findbugs:jsr305jar 包,就可以使用 @Nullable,@Nonnull,@CheckForNull 等标记注解了。

到此我们就实现了优雅避免空指针的方式:

1
2
3
4
5
public void withSmart(@Nonnull Person person){
Objects.requireNonNull(person, "person must be not null.");
Optional<Person> personOptional = Optional.of(person);
// ...
}

它能够为我们带来:

  • @Nonnull(标志注解):清晰的对外接口签名,并且能够触发 findBugs 或 idea 对代码运行期前的检查
  • Objects.requireNonNull(防御性代码):在触发边界条件时提供有意义的异常警告
  • Optional:提供优雅的业务逻辑判空实现

由于私有方法不会对外暴露,所以私有方法可以只使用 Optional类来避免 NPE

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×