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 public class SpringValidatorAdapter implements SmartValidator , javax .validation .Validator { 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);
我们可以在 Optional
实现类中找到大量 防御性代码 + Optional.of()
组合使用的应用场景,如:
1 2 3 4 5 6 7 8 public Optional<T> filter (Predicate<? super T> predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this ; else return predicate.test(value) ? this : empty(); }
基于上述的方式,我们可以基本完成一个比较优雅的避免空指针的模式了,并且当我们错误的传入空指针时,编译器(如 idea)会在运行期前及时的提醒我们方法不允许为空。那么这就够了么?还不够。在很多时候,我们会遇到遗留代码或提供三方jar
包,调用方往往会苦于无法确定传入参数是否允许为空,从而不得不研究方法实现。因此更优雅的方式是,我们对外提供的接口(public
)可以通过标记注解来对接口进行说明,而此类注解同样能触发编译器的警告。JSR 305
规范已经提供了此类注解,我们只需引入 com.google.code.findbugs:jsr305
的 jar
包,就可以使用 @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