如果你熟悉Java 8,同时又了解反应式编程(Reactive Programming)框架,例如RxJava和Reactor等,你可能会问:
“如果我可以用Java 8 的Stream, CompletableFuture, 以及Optional完成同样的事情,为什么还要用RxJava 或者 Reactor呢?”
问题在于,大多数时候你在处理的是简单的任务,这个时候你确实不需要那些反应式编程的库。但是,当系统越来越复杂,或者你处理的本身就是个复杂的任务,你恐怕就得写一些让自己头皮发麻的代码。随着时间的推移,这些代码会变得越来越复杂和难以维护。
RxJava和Reactor提供了很多非常趁手的功能,能够支持你在未来更轻松地维护你的代码,实现新需求。但是这个优势到底有多大,具体体现在哪些方面?没有标准无法比较,让我们定义8个比较的维度,来帮助我们理解Java 8的API以及反应式编程的库之间的差别。
针对上面这些维度,我们比较以下的这些类:
准备好了吗?我们开始!
上面所有的这7个类都是可组装的,支持函数式的编程方式,这是我们喜欢它们的原因。
A stream should be operated on (invoking an intermediate or terminal stream operation) only once. A stream implementation may throw IllegalStateException if it detects that the stream is being reused. However, since some stream operations may return their receiver rather than a new stream object, it may not be possible to detect reuse in all cases.
翻译过来就是:Stream只能被操作(调用中间操作或者终端操作)一次。如果一个stream的实现检测到流被重复使用了,它可以抛出一个IllegalStateException。但是因为某些流操作会返回他们的receiver,而不是一个新的stream对象,并不是在所有的情况下都能够检测出重用。
或者每次调用的时候提供一个定制的Executor。
subscribeOn让你决定用哪个Scheduler来执行Observable.create。即便你自己没有调用create,系统内部也会做类似的事情。示例:
Observable
.fromCallable(() -> {
log.info("Reading on thread: " + currentThread().getName());
return readFile("input.txt");
})
.map(text -> {
log.info("Map on thread: " + currentThread().getName());
return text.length();
})
.subscribeOn(Schedulers.io()) // <-- setting scheduler
.subscribe(value -> {
log.info("Result on thread: " + currentThread().getName());
});
输出:
Reading file on thread: RxIoScheduler-2
Map on thread: RxIoScheduler-2
Result on thread: RxIoScheduler-2
相反的,observeOn()决定在observeOn()之后,用哪个Scheduler来运行下游的执行阶段。示例:
Observable
.fromCallable(() -> {
log.info("Reading on thread: " + currentThread().getName());
return readFile("input.txt");
})
.observeOn(Schedulers.computation()) // <-- setting scheduler
.map(text -> {
log.info("Map on thread: " + currentThread().getName());
return text.length();
})
.subscribeOn(Schedulers.io()) // <-- setting scheduler
.subscribe(value -> {
log.info("Result on thread: " + currentThread().getName());
});
输出:
Reading file on thread: RxIoScheduler-2
Map on thread: RxComputationScheduler-1
Result on thread: RxComputationScheduler-1
可缓存和可重用之间的区别是什么?举个例子,我们有一个流水线A,并且使用这个流水线两次,创建两个新的流水线 B = A + 以及 C = A + 。
- 如果B和C都能成功完成,那么这个A是可重用的。
- 如果B和C都能成功完成,并且A的每一个阶段只被调用了一次,那么这个A是可缓存的。
可以看出,一个类如果是可缓存的,必然得是可重用的。
Observable work = Observable.fromCallable(() -> {
System.out.println("Doing some work");
return 10;
});
work.subscribe(System.out::println);
work.map(i -> i * 2).subscribe(System.out::println);
输出:
Doing some work
10
Doing some work
20
如果用.cache():
Observable work = Observable.fromCallable(() -> {
System.out.println("Doing some work");
return 10;
}).cache(); // <- apply caching
work.subscribe(System.out::println);
work.map(i -> i * 2).subscribe(System.out::println);
输出:
Doing some work
10
20
要做到支持反压,流水线必须是推模式的。
Backpressure(反压) 描述的是在流水线中会发生的一种场景:某些异步的阶段处理速度跟不上,需要告诉上游生产者放慢速度。直接失败是不可接受的,因为会丢失太多数据。
- Buffering(缓冲) - 把所有的onNext的值保存到缓冲区,直到下游消费它们。
- Drop Recent - 如果下游处理跟不上的话,丢弃最近的onNext值。
- Use Latest - 如果下游处理跟不上的话,只提供最近的onNext值,之前的值会被覆盖。
- None - onNext事件直接被触发,不带任何缓冲或丢弃处理。
- Exception - 如果下游处理跟不上的话,触发一个异常。
操作融合背后的想法是,在生命周期的不同点上,改变执行阶段的链条,从而消除库的架构因素所造成的额外开销。所有这些优化都是在内部处理掉的,对外部用户来说是透明的。
只有RxJava 2 和 Reactor 支持这个特性,但支持的方式不同。总的来说,有两种类型的优化:
订阅者可以向父observable轮询值。
更多的详细信息可以参考Operator-fusion (part 1) 和 Operator-fusion (part 2)
上面的内容可以总结为一个表:
总的来说,Stream, CompletableFuture, 和 Optional创建出来是为了解决特定的问题。它们解决这些问题很好用。如果它们满足你的要求,你继续用它们就好了。
但是,不同的问题有不同的复杂性。某些问题需要新的技术。RxJava 和 Reactor是一组通用的工具,帮助你用一种声明式的方式解决你面对的问题,而不是用一些并非为这种问题而提供的工具,来创建一种“hack”的解决方案。
本文主要内容翻译自:http://alexsderkach.io/comparing-java-8-rxjava-reactor/