不用null检查也能避免空指针异常

原文:https://dzone.com/articles/npe-free-code-without-null-checks-really
作者:Yogesh Devatraj
译者:Oopsguy

Optional提供了一种避免 null 检查的好方法,来看看如何使用 Optional 来处理可能为null的值。

没有使用null检查的无NPE代码

这是要开始Java代码评审么?NPE(NullPointerException)永远是Java开发人员的噩梦。让我们来看看下面这段代码:

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
35
36
37
38
39
40
41
42
public interface Service {  

public boolean switchOn(int timmer);
public boolean switchOff(int timmer);
//Other controls
}

public class RefrigeratorService implements Service {
// ...
}

public class HomeServices {

private static final int NOW = 0;
private static HomeServices service;

public static HomeServices get() {
//Null Check #1
if(service == null) {
service = new HomeServices();
}
return service;
}

public Service getRefrigertorControl() {
return new RefrigeratorService();
}

public static void main(String[] args) {
/* Get Home Services handle */
HomeServices homeServices = HomeServices.get();
//Null Check #2
if(homeServices != null) {
Service refrigertorControl = homeServices.getRefrigertorControl();
//Null Check #3
if (refrigertorControl != null) {
refrigertorControl.switchOn(NOW);
}
}
}

}

正如您看到的,代码中存在多处 null 检查。当然,这样做是为了更好地应对各种可能的场景。这是必要的,但……

有没有更好的方法?

是的!Java 8 引入了 java.util.Optional<T>。它是一个容器,可以存放可能为 null 或者不为 null 的值。Java 8 给了我们更加安全的方式来处理这些可能为 null 的对象。它受到了 Haskell 和 Scala 思想的启发。

简单地说,在值存在或者不存在的情况下,Optional 类都提供了明确处理的方法。与 null 引用相比的优势是,Optional<T> 类强制您考虑值不存的场景。因此,您可以防止意外发生空指针异常。

在上面的示例中,我们有一个家庭服务工厂,处理家庭中可用的多种设备。但这些服务不一定全部可用。这意味着它可能会发生 NullPointerException。我们可以把它包装在 Optional<Service> 中,而不是在使用任何服务之前都要添加一个判断 null 的 if 条件。

使用 Optional 包装

让我们考虑一种能从工厂中获取服务引用的方法。使用 Optonal 将服务引用包装起来,而不是直接返回服务引用。这样可以让 API 用户知道返回的服务是否可用。可以这样使用:

1
2
3
4
5
public Optional<Service> getRefrigertorControl() {
Service s = new RefrigeratorService();
//...
return Optional.ofNullable(s);
}

您可以看到,Optional.ofNullable() 提供了一个简单的方法来获取包装后的引用。当然,还有其它方法来可以获取 Optional 的引用——可以使用 Optional.empty() 或者 Optional.of()。第一个是返回一个空对象而不是 null,另一个则包装了不为 null 的对象。

这有助于避免 null 检查么?

一旦您包装了对象引用,您可以使用 Optional 提供的多个有用方法来调用没有 NPE 的包装引用。

1
2
Optional ref = homeServices.getRefrigertorControl(); 
ref.ifPresent(HomeServices::switchItOn);

如果给定的引用不是 null 值,Optional.ifPresent 将通过这个引用来调用消费者,否则,它什么也不干:

1
2
@FunctionalInterface
public interface Consumer<T>

@FunctionalInterface,这表示接受单个参数并且不返回结果的操作。与大多数其它函数式接口不同,Consumer 将通过副作用进行操作。

这很容易理解。在上述代码示例中,如果 Optional 保持引用不为 null,则会调用 HomeService.switchOn(Service)

我们经常使用三目运算符来检查 null 条件并返回替代值或者默认值。Optional 提供了另一种处理方式,而不是使用 null 检查。如果 Optional 具有 null 值,Optional.orElse(defaultObj) 将返回一个 defaultObj。让我们用代码实践一下:

1
2
3
4
public static Optional<HomeServices> get() {
service = Optional.of(service.orElse(new HomeServices()));
return service;
}

虽然现在 HomeService.get() 还是做同样的事情,但它使用了更好方式。它检查服务是否已经初始化。如果是,那么将回自身 或者创建一个新的服务。用 Optional.orElse(T) 返回默认值非常有用。

最后,以下是没有 null 检查的代码,当然也不存在 NPE:

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
35
36
import java.util.Optional;

public class HomeServices {

private static final int NOW = 0;
private static Optional<HomeServices> service;

public static Optional<HomeServices> get() {
service = Optional.of(service.orElse(new HomeServices()));
return service;
}

public Optional<Service> getRefrigertorControl() {
//Service Discovery for Refrigerator
if(ServiceDiscovery.isAvaiable("refrigetor")) {
Service s = RefrigeratorService.get();
return Optional.ofNullable(s);
}
//Possible that the service is not avaiable
return Optional.empty();
}

public static void main(String[] args) {
/* Get Home Services handle */
Optional<HomeServices> homeServices = HomeServices.get();
if(homeServices.isPresent()) {
Optional<Service> refrigertorControl = homeServices.get().getRefrigertorControl();
refrigertorControl.ifPresent(HomeServices::switchItOn);
}
}

public static void switchItOn(Service s){
//...
}

}