[响应式编程] 你真的理解Empty v.s. Null 吗

Null的原罪

众所周知,几乎所有的程序员当看到出现NPE(NullPointerException)都会异常头痛,因为有太多的原因会导致NPE的出现,排查需要耗费大量的时间,其原因是因为有null的存在。
Null的设计估计是计算机编程语言设计史上被诟病最多的一个了,其主要原因是Null存在二义性,即可以理解为“空”,也可以理解为“无效值”, 进而导致后续的程序很难正确处理,甚至还有忘记赋值后导致为null的情况。总之,一个程序总会因为各种各样的原因,导致出现了NPE问题。为了解决NPE问题,要么在程序中写很多的判空保护逻辑,要么就是后续排查会耗费大量精力,不管是哪种方式,都是一种不够经济的事情。甚至说,因为null的设计存在,导致了数十亿美金的损失。
因此,为了避免null的使用,不少语言都设计了Option的概念来避免,其含义是可能存在一个值,例如Scala的Option、Guava的Optional、Java8的Optional等。

禁止null

相比语言层面提供Optional来避免减少null的使用而言,在响应式编程中则更为彻底,直接拒绝了null的存在。根据响应式编程规范Subscriber中的第13条(Reactive Streams Specification)看:

Calling onSubscribe, onNext, onError or onComplete MUST return normally except when any provided parameter is null in which case it MUST throw a java.lang.NullPointerException to the caller, for all other situations the only legal way for a Subscriber to signal failure is by cancelling its Subscription. In the case that this rule is violated, any associated Subscription to the Subscriber MUST be considered as cancelled, and the caller MUST raise this error condition in a fashion that is adequate for the runtime environment.

可以看到,在整个流的处理中,所有方法的返回值都必须是normally的,是不能为空的。当传入null,则会直接在源头报NPE异常,例如Mono.just()、Flux.just()等。return normally的定义如下:

Only ever returns a value of the declared type to the caller. The only legal way to signal failure to a Subscriber is via the onError method.

Empty的理解

响应式编程虽然禁止了Null,但同时也加入了Empty的概念(注意: Empty在本质上是与Optional是不一样的,后面小节会细说)。前面说过Null的二义性问题:1. 不存在、空值 2. 无效值,响应式编程中通过禁止Null以及引入Empty的方式,将二义性限制到了唯一一种语义:不存在。
请牢记Empty对应的语义 —— 不存在,由于只有一种语义,所以才可以直接按照“不存在”来处理。在reactor中,有两种流类型——Mono和Flux,分别表示最多有一个数据和最多N个数据。当一个流是Empty时,则表示该数据流是空的、不存在的,所以会直接执行onCompleted()方法,从而跳过onNext的方法执行,而那些map、filter等operator其实都是属于onNext的方法执行。所以当一个流是Empty时,则所有的operator都会被跳过,直接进入onCompleted()方法,因为Empty的语义是指不存在,所以没有必要且也不能对其进行onNext的处理。

Empty的流程控制

当一个流不存在时,除了被忽略(不做任何处理外),有时候还需要进行兜底处理,这个时候可以使用switchIfEmpty来进行控制,例如:

@GetMapping(path = "/customers/{customerId}")
public Mono<Customer> findCustomerById(@PathVariable String customerId) {
    Mono<Customer> customerMono =
                customerService.findById(customerId)
                .switchIfEmpty(backupService.findById(customerId));
    return customerMono;
}

除了switchIfEmpty外,还有defaultIfEmpty方法,与switchIfEmpty不同的时,defaultIfEmpty返回的是一个value实体,不是像switchIfEmpty返回的是还是一个流。

@GetMapping(path = "/customers/{customerId}")
public Mono<ResponseEntity<Customer>> handleGetCustomer(@PathVariable String customerId) {
    Mono<ResponseEntity<Customer>> responseEntityMono = customerRepository
            .findById(customerId)
            .map(x -> new ResponseEntity<>(x, HttpStatus.OK))
            .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NO_CONTENT));
    return responseEntityMono;
}

相比之下,虽然在上面的例子中,switchIfEmpty与defaultIfEmpty用途很接近,但其实switchIfEmpty更接近“流”的概念,两者的含义是:

  • switchIfEmpty —— 当“流”为空时,切换到一个新的流
  • defaultIfEmpty —— 当“流”为空时,内部返回一个默认值来替换

前者是切换了一个新的“流”,后者则仍然是原来的流,只是把“流”内部的数据给替换(添加)了一个默认值。
这两者在Mono的使用时区别还不明显,但在Flux的使用时,则区别甚大。在这之前,先来说下Flux的empty。

理解Flux的empty

前面说过,empty表示的含义是不存在数据,所以对于Flux而言,如果一个flux是empty,那表示的就是没有一个数据,或者说是个空数据流,才能称为empty,即直接进入onCompleted()方法,不执行任何onNext的方法。
例如Flux.just(null, null, null).defaultIfEmpty(100)是不会触发defaultIfEmpty执行的,因为这里的IfEmpty指的是整个“流”是否为空,而不是“流”里面的数字是否为空。
当然,_Flux.just(null, null, null)_本身是不存在的,因为reactor中是不支持null的,这里会直接抛NPE异常,举这个例子的目的是想说明是否empty,是针对的整个数据流本身,而不是数据。

那怎么理解Flux的defaultIfEmpty行为呢?

我们知道,Flux是代表的N个数据流的,那么当为空时,只返回一个默认值,这怎么理解呢?这里,我们需要转换一下所谓的“兜底”思路,由于在响应式编程里,都是流式处理,对于源头而言,兜底其实不是一种替换,而是一种添加,是在原先空数据流的基础上,添加了有且仅有一个的数据。同理,Mono的defaultIfEmpty也是一样的行为,并不是把一个为null的value替换为一个default value,而是在空的数据流基础上添加了一个default数据,请记住:在reactor中,并不存在一个value为null的数据

Flux的元素不存在(无效值)问题

前面说了,switchIfEmpty和defaultIfEmpty处理的对象是针对空数据流的情况,那对于元素为空的情况该怎么处理呢?先看一个例子:

Flux.just(1.0, 2.0, 3.0)
  .flatMap(d -> d == 2.0 ? Mono.empty() : Mono.just(d))
  .defaultIfEmpty(100.0)
  .subscribe(d -> System.out.println(d), e -> System.err.println(e.getMessage()), () -> System.out.println("completed"));

输出结果为:

1.0
3.0
completed

我们发现,在流中value=2的数据实体,由于在后续map操作中转为了empty(类似null)后,居然最后直接消失了,而且也不会触发defaultIfEmpty方法来补充数据。
然而这里其实我们预期想要的效果其实是最终返回值是1.0, default, 3.0,而不是1.0,3.0,直接跳过了中间value=2对应的值。

为什么会出现这种现象呢?
这其实就是最早说的,null的二义性问题。在这个例子中,我们对待null的语义其实不是“空”,而是一个无效值,所以希望当出现无效值时,可以采用兜底默认值来替换。显然,Mono.empty()并不能胜任这样的功能。
那我们该如何处理才能获得类似1.0, default, 3.0的结果呢?
其实就是对处理对象增加无效值的表示,例如对于Double而言,其无效值可用NaN来表示,例如:

Flux.just(1.0, 2.0, 3.0)
 .flatMap(d -> d == 2.0 ? Mono.just(Double.NaN) : Mono.just(d))
 .subscribe(d -> System.out.println(d), e -> System.err.println(e.getMessage()), () -> System.out.println("completed"));

更实际些的例子,则是对于处理的数据类,定义一个Null Object,例如如果处理Book,则定义一个NullBook的静态值:

class Book {
    public static final NULL = new Book();
}

当处理数据时,则可用NULL来替代Mono.empty()即可实现占位兜底的效果。
从上面的例子中,可以很好的了解所谓的null的二义性,以及在Reactor中是如何禁止null以及用empty和无效值两种方式来分别表示,既解决了null的二义性并仍确保逻辑完备。

Mono v.s. Optional

最后再来分析下Mono与Optional的区别。从构造方法上看,Mono与Optional几乎没有区别,例如:

Mono.just()Optional.of()
Mono.empty()Optional.empty()
Mono.justOrEmpty()Optional.ofNullable()

甚至在单个实例的使用方式上,也几乎没有区别,例如Optional的map方法,当是empty的时候,也会跳过执行。
但即使这样(看起来用起来都一样),两者仍然有本质的区别,Mono.empty()仍然是一个流(空数据流),且只有这一种语义,不存在无效值的语义可能。而Optional是一个包装类,其语义是包含着一个数据的类,其数据有可能为null,即Optional的empty仍然具有二义性,可能是空值可能是无效值。Optional.empty()只是完成了对null的封装,避免了直接对null进行操作。

从实现方式也可以看出来,Mono.empty()返回的是一个MonoEmpty类的对象实例,Mono.just()则返回的是MonoJust对象实例。而Optional的empty()和of()则都是Optional实例,Optional.empty()的内部实现是对value == null一种判断。
对比上面的例子,类比Flux和Mono.empty(), 采用Stream和Optional来实现,会发现是无法使用flatmap的,其原因是Optional并不是一种Stream:

//类型不匹配,无法使用flatmap, 返回的Optional不是Stream类型
Stream.of(1, 2, 3).flatmap(v -> {
  if (v == 2) {
    return Optional.empty();
  }
  return Optional.of(v);
})

也因此可以看出,Optional.empty()无法原生的表示“不存在”这种语义,即无法产生那种只返回1,3的效果。
注:其实与Mono.empty()等同的,其实是Stream.empty()。

总结

本文重点介绍了在响应式编程中比较被忽略但容易误用的一个知识点,关于Null与Empty的语义理解,同时也介绍了Null的二义性问题以及在Reactor中是如何禁止null以及用empty和无效值两种方式来分别表示,既解决了null的二义性并仍确保逻辑完备。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值