Java8-反应式编程学习指南-全-

Java8 反应式编程学习指南(全)

原文:zh.annas-archive.org/md5/A4E30A017482EBE61466A691985993DC

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

响应式编程已经存在几十年了。自 Smalltalk 语言年轻时起,就有一些响应式编程的实现。然而,它最近才变得流行,并且现在成为一种趋势。你会问为什么现在?因为它适合编写快速、实时应用程序,当前的技术和 Web 需求如此。

我是在 2008 年参与其中的,当时我所在的团队正在开发一个名为 Sophie 2 的多媒体图书创建器。它必须快速响应,因此我们创建了一个名为 Prolib 的框架,它提供了可以相互依赖的对象属性(换句话说,我们为 Swing 实现了绑定等等)。将模型数据与 GUI 连接起来就像这样自然而然。

当然,这远非 RX 所具有的函数式方法。2010 年,微软发布了 RX,之后 Netflix 将其移植到 Java—RxJava。然而,Netflix 将 RxJava 发布给开源社区,该项目取得了巨大成功。许多其他语言都有其 RX 端口以及许多替代方案。现在,您可以在 Java 后端上使用响应式编程进行编码,并将其连接到 RxJava 的前端。

这本书试图向您解释响应式编程的全部内容以及如何在 RxJava 中使用它。它有许多小例子,并以小步骤解释概念和 API 细节。阅读本书后,您将对 RxJava、函数式编程和响应式范式有所了解。

本书涵盖的内容

第一章,响应式编程简介,将向您介绍响应式编程的概念,并告诉您为什么应该了解它。本章包含演示 RxJava 如何融合响应式编程概念的示例。

第二章,使用 Java 8 的函数式构造,将教您如何使用 Java 8 的新 lambda 构造。它将解释一些函数式编程概念,并向您展示如何在响应式程序中与 RxJava 一起使用它们。

第三章,创建和连接 Observables、Observers 和 Subjects,将向您展示 RxJava 库的基本构建模块,称为 Observables。您将学习“热”和“冷”Observables 之间的区别,以及如何使用订阅实例订阅和取消订阅它们。

第四章,转换、过滤和累积您的数据,将引导您了解基本的响应式操作符,您将学习如何使用它们来实现逐步计算。本章将让您了解如何转换 Observables 发出的事件,如何仅筛选出我们需要的数据,以及如何对其进行分组、累积和处理。

第五章,组合器、条件和错误处理,将向您介绍更复杂的响应式操作符,这将使您能够掌握可观察链。您将了解组合和条件操作符以及 Observables 如何相互交互。本章演示了不同的错误处理方法。

第六章,使用调度程序进行并发和并行处理,将指导您通过 RxJava 编写并发和并行程序的过程。这将通过 RxJava 调度程序来实现。将介绍调度程序的类型,您将了解何时以及为什么要使用每种调度程序。本章将向您介绍一种机制,向您展示如何避免和应用背压。

第七章,《测试您的 RxJava 应用程序》,将向您展示如何对 RxJava 应用程序进行单元测试。

第八章,《资源管理和扩展 RxJava》,将教您如何管理 RxJava 应用程序使用的数据源资源。我们将在这里编写自己的 Observable 操作符。

您需要为本书做好准备

为了运行示例,您需要:

本书适合对象

如果您是一名懂得如何编写软件并希望学习如何将现有技能应用于响应式编程的 Java 开发人员,那么这本书适合您。

这本书对任何人都有帮助,无论是初学者、高级程序员,甚至是专家。您不需要具有 Java 8 的 lambda 和 stream 或 RxJava 的任何经验。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"我们可以通过使用include指令包含其他上下文。"

代码块设置如下:

Observable
  .just('R', 'x', 'J', 'a', 'v', 'a')
  .subscribe(
    System.out::print,
    System.err::println,
    System.out::println
  );

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

Observable<Object> obs = Observable
 .interval(40L, TimeUnit.MILLISECONDS)
 .switchMap(v ->
 Observable
 .timer(0L, 10L, TimeUnit.MILLISECONDS)
 .map(u -> "Observable <" + (v + 1) + "> : " + (v + u)))
 );
subscribePrint(obs, "switchMap");

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:"这种类型的接口称为函数接口。"

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这种方式出现。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对本书的看法—您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它有助于我们开发出您真正能从中获益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在主题中提及书名。

如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的自豪所有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的帐户中为您购买的所有 Packt Publishing 图书下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接将文件发送到您的电子邮件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误是难免的。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书,点击勘误提交表链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书籍的勘误列表中的勘误部分。

要查看先前提交的勘误,请转到www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将出现在勘误部分下。

盗版

互联网上盗版受版权保护的材料是所有媒体的持续问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您帮助保护我们的作者和我们为您提供有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章:响应式编程简介

如今,“响应式编程”这个术语正处于流行之中。各种编程语言中都出现了库和框架。有关响应式编程的博客文章、文章和演示正在被创建。Facebook、SoundCloud、Microsoft 和 Netflix 等大公司正在支持和使用这个概念。因此,我们作为程序员开始思考。为什么人们对响应式编程如此兴奋?成为响应式意味着什么?它对我们的项目有帮助吗?我们应该学习如何使用它吗?

与此同时,Java 以其多线程、速度、可靠性和良好的可移植性而备受欢迎。它用于构建各种应用程序,从搜索引擎、数据库到在服务器集群上运行的复杂 Web 应用程序。但 Java 也有不好的声誉——仅使用内置工具编写并发和简单应用程序非常困难,而且在 Java 中编程需要编写大量样板代码。此外,如果需要是异步的(例如使用 futures),你很容易陷入“回调地狱”,这实际上对所有编程语言都成立。

换句话说,Java 很强大,你可以用它创建出色的应用程序,但这并不容易。好消息是,有一种方法可以改变这种情况,那就是使用响应式编程风格。

本书将介绍RxJavagithub.com/ReactiveX/RxJava),这是响应式编程范式的开源 Java 实现。使用 RxJava 编写代码需要一种不同的思维方式,但它将使您能够使用简单的结构化代码片段创建复杂的逻辑。

在本章中,我们将涵盖:

  • 响应式编程是什么

  • 学习和使用这种编程风格的原因

  • 设置 RxJava 并将其与熟悉的模式和结构进行比较

  • 使用 RxJava 的一个简单例子

什么是响应式编程?

响应式编程是围绕变化的传播而展开的一种范式。换句话说,如果一个程序将修改其数据的所有变化传播给所有感兴趣的方,那么这个程序就可以被称为响应式

微软 Excel 就是一个简单的例子。如果在单元格 A1 中设置一个数字,在单元格'B1'中设置另一个数字,并将单元格'C1'设置为SUM(A1, B1);每当'A1'或'B1'发生变化时,'C1'将被更新为它们的和。

让我们称之为响应式求和

将简单变量c分配为ab变量的和与响应式求和方法之间有什么区别?

在普通的 Java 程序中,当我们改变'a'或'b'时,我们必须自己更新'c'。换句话说,由'a'和'b'表示的数据流的变化不会传播到'c'。下面通过源代码进行了说明:

int a = 4;
int b = 5;
int c = a + b;
System.out.println(c); // 9

a = 6;
System.out.println(c);
// 9 again, but if 'c' was tracking the changes of 'a' and 'b',
// it would've been 6 + 5 = 11

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接将文件发送到您的电子邮件。

这是对“响应式”意味着什么的非常简单的解释。当然,这个想法有各种实现,也有各种问题需要这些实现来解决。

为什么我们应该是响应式的?

我们回答这个问题最简单的方法是考虑我们在构建应用程序时的需求。

10-15 年前,网站经过维护或响应时间缓慢是正常的,但今天一切都应该 24/7 在线,并且应该以闪电般的速度响应;如果慢或宕机,用户会选择另一个服务。今天慢意味着无法使用或损坏。我们正在处理更大量的数据,需要快速提供和处理。

HTTP 故障在最近过去并不罕见,但现在,我们必须具有容错能力,并为用户提供可读和合理的消息更新。

过去,我们编写简单的桌面应用程序,但今天我们编写应该快速响应的 Web 应用程序。在大多数情况下,这些应用程序与大量远程服务进行通信。

这些是我们必须满足的新要求,如果我们希望我们的软件具有竞争力。换句话说,我们必须是:

  • 模块化/动态:这样,我们将能够拥有 24/7 系统,因为模块可以下线并上线,而不会破坏或停止整个系统。此外,这有助于我们更好地构建随着规模扩大而管理其代码库的应用程序。

  • 可扩展性:这样,我们将能够处理大量数据或大量用户请求。

  • 容错:这样,系统将对其用户显示稳定。

  • 响应性:这意味着快速和可用。

让我们考虑如何实现这一点:

  • 如果我们的系统是事件驱动,我们可以变得模块化。我们可以将系统分解为多个微服务/组件/模块,它们将使用通知相互通信。这样,我们将对系统的数据流做出反应,这些数据流由通知表示。

  • 可扩展意味着对不断增长的数据做出反应,对负载做出反应而不会崩溃。

  • 对故障/错误的反应将使系统更具容错能力。

  • 响应性意味着及时对用户活动做出反应。

如果应用程序是事件驱动的,它可以分解为多个自包含组件。这有助于我们变得更具可扩展性,因为我们可以随时添加新组件或删除旧组件,而不会停止或破坏系统。如果错误和故障传递到正确的组件,它可以将它们处理为通知,应用程序可以变得更具容错能力或弹性。因此,如果我们构建我们的系统为事件驱动,我们可以更容易地实现可扩展性和故障容忍性,而且可扩展、解耦和防错的应用程序对用户快速响应。

为什么我们应该是反应式的?

Reactive Manifestowww.reactivemanifesto.org/)是一份文件,定义了我们之前提到的四个反应原则。每个反应系统都应该是消息驱动的(事件驱动)。这样,它可以变得松散耦合,因此可扩展和具有弹性(容错),这意味着它是可靠和响应的(请参见上图)。

请注意,Reactive Manifesto 描述了一个反应式系统,并不同于我们对反应式编程的定义。您可以构建一个消息驱动、具有弹性、可扩展和响应的应用程序,而无需使用反应式库或语言。

应用程序数据的更改可以使用通知进行建模,并且可以传播到正确的处理程序。因此,使用反应式编程编写应用程序是遵守宣言的最简单方法。

介绍 RxJava

要编写响应式程序,我们需要一个库或特定的编程语言,因为自己构建这样的东西是相当困难的任务。Java 并不是一个真正的响应式编程语言(它提供了一些工具,比如java.util.Observable类,但它们相当有限)。它是一种静态类型的面向对象的语言,我们需要编写大量样板代码来完成简单的事情(例如 POJOs)。但是在 Java 中有一些我们可以使用的响应式库。在这本书中,我们将使用 RxJava(由 Java 开源社区的人员开发,由 Netflix 指导)。

下载和设置 RxJava

你可以从 Github(github.com/ReactiveX/RxJava)下载并构建 RxJava。它不需要任何依赖,并支持 Java 8 的 lambda。它的 Javadoc 和 GitHub 维基页面提供的文档结构良好,是最好的之一。以下是如何查看项目并运行构建:

$ git clone git@github.com:ReactiveX/RxJava.git
$ cd RxJava/
$ ./gradlew build

当然,你也可以下载预构建的 JAR。在这本书中,我们将使用 1.0.8 版本。

如果你使用 Maven,你可以将 RxJava 作为依赖项添加到你的pom.xml文件中:

<dependency>
  <groupId>io.reactivex</groupId>
  <artifactId>rxjava</artifactId>
  <version>1.0.8</version>
</dependency>

或者,对于 Apache Ivy,将这个片段放入你的 Ivy 文件的依赖项中:

<dependency org="io.reactivex" name="rxjava" rev="1.0.8" />

如果你使用 Gradle,你可以更新你的build.gradle文件的依赖项如下:

dependencies {
  ...
  compile 'io.reactivex:rxjava:1.0.8'
  ...
}

注意

本书附带的代码示例和程序可以使用 Gradle 构建和测试。它可以从这个 Github 仓库下载:github.com/meddle0x53/learning-rxjava

现在,让我们来看看 RxJava 到底是什么。我们将从一些众所周知的东西开始,逐渐深入到这个库的秘密中。

比较迭代器模式和 RxJava Observable

作为 Java 程序员,你很可能听说过或使用过“迭代器”模式。这个想法很简单:一个“迭代器”实例用于遍历容器(集合/数据源/生成器),在需要时逐个拉取容器的元素,直到达到容器的末尾。以下是在 Java 中如何使用它的一个小例子:

List<String> list = Arrays.asList("One", "Two", "Three", "Four", "Five"); // (1)

Iterator<String> iterator = list.iterator(); // (2)

while(iterator.hasNext()) { // 3
  // Prints elements (4)
  System.out.println(iterator.next());
}

每个java.util.Collection对象都是一个Iterable实例,这意味着它有iterator()方法。这个方法创建一个Iterator实例,它的源是集合。让我们看看前面的代码做了什么:

  1. 我们创建一个包含五个字符串的新List实例。

  2. 我们使用iterator()方法从这个List实例创建一个Iterator实例。

  3. Iterator接口有两个重要的方法:hasNext()next()hasNext()方法用于检查Iterator实例是否有更多元素可遍历。在这里,我们还没有开始遍历元素,所以它将返回True。当我们遍历这五个字符串时,它将返回False,程序将在while循环之后继续进行。

  4. 前五次调用Iterator实例的next()方法时,它将按照它们在集合中插入的顺序返回元素。所以字符串将被打印出来。

在这个例子中,我们的程序使用Iterator实例从List实例中消耗项目。它拉取数据(这里用字符串表示),当前线程会阻塞,直到请求的数据准备好并接收到。所以,例如,如果Iterator实例在每次next()方法调用时向 web 服务器发送请求,我们程序的主线程将在等待每个响应到达时被阻塞。

RxJava 的构建块是可观察对象。Observable类(请注意,这不是 JDK 中附带的java.util.Observable类)是Iterator类的数学对偶,这基本上意味着它们就像同一枚硬币的两面。它具有产生值的基础集合或计算,可以被消费者消耗。但不同之处在于,消费者不像Iterator模式中那样从生产者“拉”这些值。恰恰相反;生产者通过通知将值“推送”给消费者。

这是相同程序的示例,但使用Observable实例编写:

List<String> list = Arrays.asList("One", "Two", "Three", "Four", "Five"); // (1)

Observable<String> observable = Observable.from(list); // (2)

observable.subscribe(new Action1<String>() { // (3)
  @Override
  public void call(String element) {
    System.out.println(element); // Prints the element (4)
  }
});

以下是代码中发生的情况:

  1. 我们以与上一个示例相同的方式创建字符串列表。

  2. 然后,我们从列表中创建一个Observable实例,使用from(Iterable<? extends T> iterable)方法。此方法用于创建Observable的实例,它们将所有值同步地从Iterable实例(在我们的例子中是列表)逐个发送给它们的订阅者(消费者)。我们将在第三章中看看如何逐个将值发送给订阅者,创建和连接 Observable、Observer 和 Subject

  3. 在这里,我们可以订阅Observable实例。通过订阅,我们告诉 RxJava 我们对这个Observable实例感兴趣,并希望从中接收通知。我们使用实现Action1接口的匿名类进行订阅,通过定义一个单一方法call(T)。这个方法将由Observable实例每次有值准备推送时调用。始终创建新的Action1实例可能会显得太啰嗦,但 Java 8 解决了这种冗长。我们将在第二章中了解更多信息,使用 Java 8 的函数构造

  4. 因此,源列表中的每个字符串都将通过call()方法推送,并将被打印出来。

RxJava 的Observable类的实例行为有点像异步迭代器,它们自己通知其订阅者/消费者有下一个值。事实上,Observable类在经典的Observer模式(在 Java 中实现——参见java.util.Observable,参见《设计模式:可复用面向对象软件的元素》)中添加了Iterable类型中的两个可用的东西。

  • 向消费者发出没有更多数据可用的信号的能力。我们可以附加一个订阅者来监听“OnCompleted”通知,而不是调用hasNext()方法。

  • 信号订阅者发生错误的能力。我们可以将错误侦听器附加到Observable实例,而不是尝试捕获错误。

这些侦听器可以使用subscribe(Action1<? super T>, Action1 <Throwable>, Action0)方法附加。让我们通过添加错误和完成侦听器来扩展Observable实例示例:

List<String> list = Arrays.asList("One", "Two", "Three", "Four", "Five");

Observable<String> observable = Observable.from(list);
observable.subscribe(new Action1<String>() {
  @Override
  public void call(String element) {
    System.out.println(element);
  }
},
new Action1<Throwable>() {
 @Override
 public void call(Throwable t) {
 System.err.println(t); // (1)
 }
},
new Action0() {
 @Override
 public void call() {
 System.out.println("We've finnished!"); // (2)
 }
});

新的东西在这里是:

  1. 如果在处理元素时出现错误,Observable实例将通过此侦听器的call(Throwable)方法发送此错误。这类似于Iterator实例示例中的 try-catch 块。

  2. 当一切都完成时,Observable实例将调用此call()方法。这类似于使用hasNext()方法来查看Iterable实例的遍历是否已经完成并打印“We've finished!”。

注意

此示例可在 GitHub 上查看,并可在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter01/ObservableVSIterator.java上查看/下载。

我们看到了如何使用Observable实例,它们与我们熟悉的Iterator实例并没有太大的不同。这些Observable实例可以用于构建异步流,并将数据更新推送给它们的订阅者(它们可以有多个订阅者)。这是反应式编程范式的一种实现。数据被传播给所有感兴趣的方,即订阅者。

使用这样的流进行编码是反应式编程的更类似函数式的实现。当然,对此有正式的定义和复杂的术语,但这是最简单的解释。

订阅事件应该是熟悉的;例如,在 GUI 应用程序中点击按钮会触发一个事件,该事件会传播给订阅者—处理程序。但是,使用 RxJava,我们可以从任何地方创建数据流—文件输入、套接字、响应、变量、缓存、用户输入等等。此外,消费者可以被通知流已关闭,或者发生了错误。因此,通过使用这些流,我们的应用程序可以对失败做出反应。

总之,流是一系列持续的消息/事件,按照它们在实时处理中的顺序排序。它可以被看作是随着时间变化的值,这些变化可以被依赖它的订阅者(消费者)观察到。因此,回到 Excel 的例子,我们实际上用"反应式变量"或 RxJava 的Observable实例有效地替换了传统变量。

实现反应式求和

现在我们熟悉了Observable类和如何以反应式方式使用它编码的想法,我们准备实现在本章开头提到的反应式求和。

让我们看看我们的程序必须满足的要求:

  • 它将是一个在终端中运行的应用程序。

  • 一旦启动,它将一直运行,直到用户输入exit

  • 如果用户输入a:<number>a收集器将更新为

  • 如果用户输入b:<number>b收集器将更新为

  • 如果用户输入其他内容,将被跳过。

  • ab收集器都有初始值时,它们的和将自动计算并以a + b = 的格式打印在标准输出上。在ab的每次更改时,和将被更新并打印。

源代码包含了我们将在接下来的四章中详细讨论的功能。

第一段代码代表程序的主体:

ConnectableObservable<String> input = from(System.in); // (1)

Observable<Double> a = varStream("a", input); (2)
Observable<Double> b = varStream("b", input);

ReactiveSum sum = new ReactiveSum(a, b); (3)

input.connect(); (4)

这里发生了很多新的事情:

  1. 我们必须做的第一件事是创建一个代表标准输入流(System.in)的Observable实例。因此,我们使用from(InputStream)方法(实现将在下一个代码片段中呈现)从System.in创建一个ConnectableObservable变量。ConnectableObservable变量是一个Observable实例,只有在调用其connect()方法后才开始发出来自其源的事件。在第三章中详细了解它,创建和连接 Observables、Observers 和 Subjects

  2. 我们使用varStream(String, Observable)方法创建代表ab值的两个Observable实例,我们将在后面进行详细讨论。这些值的源流是输入流。

  3. 我们创建了一个ReactiveSum实例,依赖于ab的值。

  4. 现在,我们可以开始监听输入流了。

这段代码负责在程序中建立依赖关系并启动它。ab的值依赖于用户输入,它们的和也依赖于它们。

现在让我们看看from(InputStream)方法的实现,它创建了一个带有java.io.InputStream源的Observable实例:

static ConnectableObservable<String> from(final InputStream stream) {
  return from(new BufferedReader(new InputStreamReader(stream)));// (1)
}

static ConnectableObservable<String> from(final BufferedReader reader) {
  return Observable.create(new OnSubscribe<String>() { // (2)
    @Override
    public void call(Subscriber<? super String> subscriber) {
      if (subscriber.isUnsubscribed()) {  // (3)
        return;
      }
      try {
        String line;
        while(!subscriber.isUnsubscribed() &&
          (line = reader.readLine()) != null) { // (4)
            if (line == null || line.equals("exit")) { // (5)
              break;
            }
            subscriber.onNext(line); // (6)
          }
        }
        catch (IOException e) { // (7)
          subscriber.onError(e);
        }
        if (!subscriber.isUnsubscribed()) // (8)
        subscriber.onCompleted();
      }
    }
  }).publish(); // (9)
}

这是一段复杂的代码,让我们一步一步来看:

  1. 这个方法的实现将它的InputStream参数转换为BufferedReader对象,并调用from(BufferedReader)方法。我们这样做是因为我们将使用字符串作为数据,并且使用Reader实例更容易。

  2. 因此,实际的实现在第二个方法中。它返回一个Observable实例,使用Observable.create(OnSubscribe)方法创建。这个方法是我们在本书中将要经常使用的方法。它用于创建具有自定义行为的Observable实例。传递给它的rx.Observable.OnSubscribe接口有一个方法,call(Subscriber)。这个方法用于实现Observable实例的行为,因为传递给它的Subscriber实例可以用于向Observable实例的订阅者发出消息。订阅者是Observable实例的客户端,它消耗它的通知。在第三章中了解更多信息,创建和连接 Observables、Observers 和 Subjects

  3. 如果订阅者已经取消订阅了这个Observable实例,就不应该做任何事情。

  4. 主要逻辑是监听用户输入,同时订阅者已经订阅。用户在终端输入的每一行都被视为一条消息。这是程序的主循环。

  5. 如果用户输入单词exit并按下Enter,主循环将停止。

  6. 否则,用户输入的消息将通过onNext(T)方法作为通知传递给Observable实例的订阅者。这样,我们将一切都传递给感兴趣的各方。他们的工作是过滤和转换原始消息。

  7. 如果发生 IO 错误,订阅者将通过onError(Throwable)方法收到一个OnError通知。

  8. 如果程序到达这里(通过跳出主循环),并且订阅者仍然订阅了Observable实例,将使用onCompleted()方法向订阅者发送一个OnCompleted通知。

  9. 使用publish()方法,我们将新的Observable实例转换为ConnectableObservable实例。我们必须这样做,否则,对于对这个Observable实例的每次订阅,我们的逻辑将从头开始执行。在我们的情况下,我们希望只执行一次,并且所有订阅者都收到相同的通知;这可以通过使用ConnectableObservable实例来实现。在第三章中了解更多信息,创建和连接 Observables、Observers 和 Subjects

这说明了将 Java 的 IO 流简化为Observable实例的方法。当然,使用这个主循环,程序的主线程将阻塞等待用户输入。可以使用正确的Scheduler实例将逻辑移动到另一个线程来防止这种情况发生。我们将在第六章中重新讨论这个话题,使用调度程序进行并发和并行处理

现在,用户在终端输入的每一行都会被这个方法创建的ConnectableObservable实例传播为一个通知。现在是时候看看我们如何将代表总和收集器的值Observable实例连接到这个输入Observable实例了。这是varStream(String, Observable)方法的实现,它接受一个值的名称和源Observable实例,并返回代表这个值的Observable实例:

public static Observable<Double> varStream(final String varName, Observable<String> input) {
  final Pattern pattern = Pattern.compile("\\^s*" + varName + "\\s*[:|=]\\s*(-?\\d+\\.?\\d*)$"); // (1)
  return input
  .map(new Func1<String, Matcher>() {
    public Matcher call(String str) {
      return pattern.matcher(str); // (2)
    }
  })
  .filter(new Func1<Matcher, Boolean>() {
    public Boolean call(Matcher matcher) {
      return matcher.matches() && matcher.group(1) != null; // (3)
    }
  })
  .map(new Func1<Matcher, Double>() {
    public Double call(Matcher matcher) {
      return Double.parseDouble(matcher.group(1)); // (4)
    }
  });
}

在这里调用的map()filter()方法是 RxJava 提供的流畅 API 的一部分。它们可以在Observable实例上调用,创建一个依赖于这些方法的新的Observable实例,用于转换或过滤传入的数据。通过正确使用这些方法,您可以通过一系列步骤表达复杂的逻辑,以达到您的目标。在第四章中了解更多信息,转换、过滤和累积您的数据。让我们分析一下代码:

  1. 我们的变量只对格式为<var_name>: <value><var_name> = <value>的消息感兴趣,因此我们将使用这个正则表达式来过滤和处理这些类型的消息。请记住,我们的输入Observable实例会发送用户写的每一行;我们的工作是以正确的方式处理它。

  2. 使用我们从输入接收的消息,我们使用前面的正则表达式作为模式创建了一个Matcher实例。

  3. 我们只通过与正则表达式匹配的数据。其他一切都被丢弃。

  4. 这里要设置的值被提取为Double数值。

这就是值ab如何通过双值流表示,随时间变化。现在我们可以实现它们的总和。我们将其实现为一个实现了Observer接口的类,因为我想向您展示订阅Observable实例的另一种方式——使用Observer接口。以下是代码:

public static final class ReactiveSum implements Observer<Double> { // (1)
  private double sum;
  public ReactiveSum(Observable<Double> a, Observable<Double> b) {
    this.sum = 0;
    Observable.combineLatest(a, b, new Func2<Double, Double, Double>() { // (5)
      public Double call(Double a, Double b) {
        return a + b;
      }
    }).subscribe(this); // (6)
  }
  public void onCompleted() {
    System.out.println("Exiting last sum was : " + this.sum); // (4)
  }
  public void onError(Throwable e) {
    System.err.println("Got an error!"); // (3)
    e.printStackTrace();
  }
  public void onNext(Double sum) {
    this.sum = sum;
    System.out.println("update : a + b = " + sum); // (2)
  }
}

这是实际总和的实现,依赖于表示其收集器的两个Observable实例:

  1. 这是一个Observer接口。Observer实例可以传递给Observable实例的subscribe(Observer)方法,并定义了三个方法,这些方法以三种类型的通知命名:onNext(T)onError(Throwable)onCompleted。在第三章中了解更多关于这个接口的信息,创建和连接 Observables、Observers 和 Subjects

  2. 在我们的onNext(Double)方法实现中,我们将总和设置为传入的值,并在标准输出中打印更新。

  3. 如果我们遇到错误,我们只是打印它。

  4. 当一切都完成时,我们用最终的总和向用户致以问候。

  5. 我们使用combineLatest(Observable, Observable, Func2)方法实现总和。这个方法创建一个新的Observable实例。当传递给 combineLatest 的两个Observable实例中的任何一个接收到更新时,新的Observable实例将被更新。通过新的Observable实例发出的值是由第三个参数计算的——这个函数可以访问两个源序列的最新值。在我们的情况下,我们将这些值相加。只有当传递给该方法的两个Observable实例都至少发出一个值时,才会收到通知。因此,只有当ab都有通知时,我们才会得到总和。在第五章中了解更多关于这个方法和其他组合器的信息,组合器、条件和错误处理

  6. 我们将我们的Observer实例订阅到组合的Observable实例上。

这是这个示例的输出可能看起来像的样本:

Reacitve Sum. Type 'a: <number>' and 'b: <number>' to try it.
a:4
b:5
update : a + b = 9.0
a:6
update : a + b = 11.0

就是这样!我们使用数据流实现了我们的响应式总和。

注意

这个示例的源代码可以从这里下载并尝试:github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter01/ReactiveSumV1.java

总结

在本章中,我们已经了解了响应式原则以及学习和使用它们的原因。构建一个响应式应用并不难;它只需要将程序结构化为一系列小的声明式步骤。通过 RxJava,可以通过构建多个正确连接的异步流来实现这一点,从而在整个数据传输过程中转换数据。

本章介绍的两个例子乍一看可能有点复杂和令人困惑,但实际上它们非常简单。它们中有很多新东西,但在接下来的章节中将会详细解释一切。

如果您想阅读更多关于响应式编程的内容,请查看《在 Netflix API 中使用 RxJava 进行响应式编程》这篇精彩的文章,可在techblog.netflix.com/2013/02/rxjava-netflix-api.html上找到。另一篇介绍这一概念的精彩文章可以在这里找到:gist.github.com/staltz/868e7e9bc2a7b8c1f754

这些是由 RxJava 的创造者之一 Ben Christensen 制作的有关响应式编程和 RX 的幻灯片:speakerdeck.com/benjchristensen/reactive-programming-with-rx-at-qconsf-2014

在下一章中,我们将讨论一些关于函数式编程的概念及其在 Java 8 中的实现。这将为我们提供在接下来的章节中所需的基本思想,并帮助我们摆脱在编写响应式程序时的 Java 冗长性。

第二章:使用 Java 8 的函数式构造

函数式编程并不是一个新的想法;实际上,它相当古老。例如,Lisp是一种函数式语言,是当今常用编程语言中第二古老的语言。

函数式程序是使用可重用的纯函数(lambda)构建的。程序逻辑由小的声明性步骤组成,而不是复杂的算法。这是因为函数式程序最小化了状态的使用,这使得命令式程序复杂且难以重构/支持。

Java 8 带来了 lambda 表达式和将函数传递给函数的能力。有了它们,我们可以以更函数式的风格编码,并摆脱大量的样板代码。Java 8 带来的另一个新功能是流——与 RxJava 的可观察对象非常相似,但不是异步的。结合这些流和 lambda,我们能够创建更类似函数式的程序。

我们将熟悉这些新的构造,并看看它们如何与 RxJava 的抽象一起使用。通过使用 lambda,我们的程序将更简单,更容易跟踪,并且本章介绍的概念将有助于设计应用程序。

本章涵盖:

  • Java 8 中的 Lambda

  • 使用 lambda 语法的第一个 RxJava 示例

  • 纯函数和高阶函数是什么

Java 8 中的 Lambda

Java 8 中最重要的变化是引入了 lambda 表达式。它们使编码更快,更清晰,并且可以使用函数式编程。

Java 是在 90 年代作为面向对象的编程语言创建的,其思想是一切都应该是一个对象。那时,面向对象编程是软件开发的主要范式。但是,最近,函数式编程因其适用于并发和事件驱动编程而变得越来越受欢迎。这并不意味着我们应该停止使用面向对象的语言编写代码。相反,最好的策略是将面向对象和函数式编程的元素混合在一起。将 lambda 添加到 Java 8 符合这个想法——Java 是一种面向对象的语言,但现在它有了 lambda,我们也能够使用函数式风格编码。

让我们详细看看这个新功能。

介绍新的语法和语义

为了介绍 lambda 表达式,我们需要看到它们的实际价值。这就是为什么本章将以一个不使用 lambda 表达式实现的示例开始,然后重新使用 lambda 表达式实现相同的示例。

还记得Observable类中的map(Func1)方法吗?让我们尝试为java.util.List集合实现类似的东西。当然,Java 不支持向现有类添加方法,因此实现将是一个接受列表和转换并返回包含转换元素的新列表的静态方法。为了将转换传递给方法,我们将需要一个表示它的方法的接口。

让我们来看看代码:

interface Mapper<V, M> { // (1)
  M map(V value); // (2)
}

// (3)	
public static <V, M> List<M> map(List<V> list, Mapper<V, M> mapper) {
  List<M> mapped = new ArrayList<M>(list.size()); // (4)
  for (V v : list) {
    mapped.add(mapper.map(v)); // (5)
  }
  return mapped; // (6)
}

这里发生了什么?

  1. 我们定义了一个名为Mapper的通用接口。

  2. 它只有一个方法,M map(V),它接收一个类型为V的值并将其转换为类型为M的值。

  3. 静态方法List<M> map(List<V>, Mapper<V, M>)接受一个类型为V的元素列表和一个Mapper实现。使用这个Mapper实现的map()方法对源列表的每个元素进行转换,将列表转换为包含转换元素的新类型为M的列表。

  4. 该实现创建一个新的空类型为M的列表,其大小与源列表相同。

  5. 使用传递的Mapper实现转换源列表中的每个元素,并将其添加到新列表中。

  6. 返回新列表。

在这个实现中,每当我们想通过转换另一个列表创建一个新列表时,我们都必须使用正确的转换来实现Mapper接口。直到 Java 8,将自定义逻辑传递给方法的正确方式正是这样——使用匿名类实例,实现给定的方法。

但让我们看看我们如何使用这个List<M> map(List<V>, Mapper<V, M>)方法:

List<Integer> mapped = map(numbers, new Mapper<Integer, Integer>() {
  @Override
  public Integer map(Integer value) {
    return value * value; // actual mapping
  }
});

为了对列表应用映射,我们需要编写四行样板代码。实际的映射非常简单,只有其中一行。真正的问题在于,我们传递的不是一个操作,而是一个对象。这掩盖了这个程序的真正意图——传递一个从源列表的每个项目产生转换的操作,并在最后得到一个应用了变化的列表。

这是使用 Java 8 的新 lambda 语法进行的调用的样子:

List<Integer> mapped = map(numbers, value -> value * value);

相当直接了当,不是吗?它只是起作用。我们不是传递一个对象并实现一个接口,而是传递一块代码,一个无名函数。

发生了什么?我们定义了一个任意的接口和一个任意的方法,但我们可以在接口的实例位置传递这个 lambda。在 Java 8 中,如果您定义了只有一个抽象方法的接口,并且创建了一个接收此类型接口参数的方法,那么您可以传递 lambda。如果接口的单个方法接受两个字符串类型的参数并返回整数值,那么 lambda 将必须由->之前的两个参数组成,并且为了返回整数,参数将被推断为字符串。

这种类型的接口称为功能接口。单个方法是抽象的而不是默认的非常重要。Java 8 中的另一件新事物是接口的默认方法:

interface Program {
  default String fromChapter() {
    return "Two";
  }
}

默认方法在更改已经存在的接口时非常有用。当我们向它们添加默认方法时,实现它们的类不会中断。只有一个默认方法的接口不是功能性的;单个方法不应该是默认的。

Lambda 充当功能接口的实现。因此,可以将它们分配给接口类型的变量,如下所示:

Mapper<Integer, Integer> square = (value) -> value * value;

我们可以重复使用 square 对象,因为它是Mapper接口的实现。

也许您已经注意到了,但在目前为止的例子中,lambda 表达式的参数没有类型。那是因为类型是被推断的。因此,这个表达式与前面的表达式完全相同:

Mapper<Integer, Integer> square = (Integer value) -> value * value;

没有类型的 lambda 表达式的参数是如何工作的并不是魔术。Java 是一种静态类型语言,因此功能接口的单个方法的参数用于类型检查。

那么 lambda 表达式的主体呢?任何地方都没有return语句。事实证明,这两个例子完全相同:

Mapper<Integer, Integer> square = (value) -> value * value;
// and
Mapper<Integer, Integer> square = (value) -> {
 return value * value;
};

第一个表达式只是第二个的简写形式。最好 lambda 只有一行代码。但是如果 lambda 表达式包含多行,定义它的唯一方法是使用第二种方法,就像这样:

Mapper<Integer, Integer> square = (value) -> {
  System.out.println("Calculating the square of " + value);
  return value * value;
};

在底层,lambda 表达式不仅仅是匿名内部类的语法糖。它们被实现为在Java 虚拟机JVM)内快速执行,因此如果您的代码只设计为与 Java 8+兼容,那么您应该绝对使用它们。它们的主要思想是以与数据传递相同的方式传递行为。这使得您的程序更易读。

与新语法相关的最后一件事是能够传递到方法并分配给已定义的函数和方法。让我们定义一个新的功能接口:

interface Action<V> {
  void act(V value);
}

我们可以使用它来对列表中的每个值执行任意操作;例如,记录列表。以下是使用此接口的方法:

public static <V> void act(List<V> list, Action<V> action) {
  for (V v : list) {
    action.act(v);
  }
}

这个方法类似于map()函数。它遍历列表并在每个元素上调用传递的动作的act()方法。让我们使用一个简单记录列表中元素的 lambda 来调用它:

act(list, value -> System.out.println(value));

这很简单,但不是必需的,因为println()方法本身可以直接传递给act()方法。这样做如下:

act(list, System.out::println);

注意

这些示例的代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter02/Java8LambdasSyntaxIntroduction.java上查看/下载。

这是 Java 8 中的有效语法——每个方法都可以成为 lambda,并且可以分配给一个变量或传递给一个方法。所有这些都是有效的:

  • Book::makeBook // 类的静态方法

  • book::read // 实例的方法

  • Book::new // 类的构造函数

  • Book::read // 实例方法,但在没有使用实际实例的情况下引用。

现在我们已经揭示了 lambda 语法,我们将在 RxJava 示例中使用它,而不是匿名内部类。

Java 8 和 RxJava 中的函数接口

Java 8 带有一个特殊的包,其中包含常见情况的函数接口。这个包是java.util.function,我们不会在本书中详细介绍它,但会介绍一些值得一提的接口:

  • Consumer<T>:这代表接受一个参数并返回空的函数。它的抽象方法是void accept(T)。例如,我们可以将System.out::println方法分配给一个变量,如下所示:
Consumer<String> print = System.out::println;
  • Function<T,R>:这代表接受给定类型的一个参数并返回任意类型结果的函数。它的抽象方法是R accept(T),可以用于映射。我们根本不需要Mapper接口!让我们看一下以下代码片段:
Function<Integer, String> toStr = (value) -> (value + "!");
List<String> string = map(integers, toStr);
  • Predicate<T>:这代表只有一个参数并返回布尔结果的函数。它的抽象方法是boolean test(T),可以用于过滤。让我们看一下以下代码:
Predicate<Integer> odd = (value) -> value % 2 != 0;

还有许多类似的函数接口;例如,带有两个参数的函数,或者二元运算符。这又是一个带有两个参数的函数,但两个参数类型相同,并返回相同类型的结果。它们有助于在我们的代码中重用 lambda。

好处是 RxJava 与 lambda 兼容。这意味着我们传递给subscribe方法的动作实际上是函数接口!

RxJava 的函数接口在rx.functions包中。它们都扩展了一个基本的标记 接口(没有方法的接口,用于类型检查),称为Function。此外,还有另一个标记接口,扩展了Function,称为Action。它用于标记消费者(返回空的函数)。

RxJava 有十一个Action接口:

Action0 // Action with no parameters
Action1<T1> // Action with one parameter
Action2<T1,T2> // Action with two parameters
Action9<T1,T2,T3,T4,T5,T6,T7,T8,T9> // Action with nine parameters
ActionN // Action with arbitrary number of parameters

它们主要用于订阅(Action1Action0)。我们在第一章中看到的Observable.OnSubscribe<T>参数(用于创建自定义可观察对象)也扩展了Action接口。

类似地,有十一个Function扩展器代表返回结果的函数。它们是Func0<R>Func1<T1, R>... Func9<T1,T2,T3,T4,T5,T6,T7,T8,T9,R>FuncN<R>。它们用于映射、过滤、组合和许多其他目的。

RxJava 中的每个操作符和订阅方法都适用于一个或多个这些接口。这意味着我们几乎可以在 RxJava 的任何地方使用 lambda 表达式代替匿名内部类。从这一点开始,我们所有的示例都将使用 lambda,以便更易读和有些函数式。

现在,让我们看一个使用 lambda 实现的大型 RxJava 示例。这是我们熟悉的响应式求和示例!

使用 lambda 实现响应式求和示例

因此,这次,我们的主要代码片段将与之前的相似:

ConnectableObservable<String> input = CreateObservable.from(System.in);

Observable<Double> a = varStream("a", input);
Observable<Double> b = varStream("b", input);

reactiveSum(a, b); // The difference

input.connect();

唯一的区别是我们将采用更加功能性的方法来计算我们的总和,而不是保持相同的状态。我们不会实现Observer接口;相反,我们将传递 lambda 来订阅。这个解决方案更加清晰。

CreateObservable.from(InputStream)方法与我们之前使用的非常相似。我们将跳过它,看看Observable<Double> varStream(String, Observable<String>)方法,它创建了代表收集器的Observable实例:

public static Observable<Double> varStream(
  final String name, Observable<String> input) {
    final Pattern pattern =     Pattern.compile(
      "\\s*" + name + "\\s*[:|=]\\s*(-?\\d+\\.?\\d*)$"
    );
    return input
    .map(pattern::matcher) // (1)
 .filter(m -> m.matches() && m.group(1) != null) // (2)
 .map(matcher -> matcher.group(1)) // (3)
 .map(Double::parseDouble); // (4)
  }
)

这个方法比以前使用的要短得多,看起来更简单。但从语义上讲,它是相同的。它创建了一个与源可观察对象连接的Observable实例,该源可观察对象产生任意字符串,如果字符串符合它期望的格式,它会提取出一个双精度数并发出这个数字。负责检查输入格式和提取数字的逻辑只有四行,由简单的 lambda 表示。让我们来看一下:

  1. 我们映射一个 lambda,使用预期的模式和输入字符串创建一个matcher实例。

  2. 使用filter()方法,只过滤正确格式的输入。

  3. 使用map()操作符,我们从matcher实例中创建一个字符串,其中只包含我们需要的数字数据。

  4. 再次使用map()操作符,将字符串转换为双精度数。

至于新的void reactiveSum(Observable<Double>, Observable<Double>)方法的实现,请使用以下代码:

public static void reactiveSum(
  Observable<Double> a,
  Observable<Double> b) {
    Observable
      .combineLatest(a, b, (x, y) -> x + y) // (1)
 .subscribe( // (2)
 sum -> System.out.println("update : a + b = " + sum),
 error -> {
 System.out.println("Got an error!");
 error.printStackTrace();
 },
 () -> System.out.println("Exiting...")
 );
}

让我们看一下以下代码:

  1. 再次使用combineLatest()方法,但这次第三个参数是一个简单的 lambda,实现了求和。

  2. subscribe()方法接受三个 lambda 表达式,当发生以下事件时触发:

  • 总和改变了

  • 有一个错误

  • 程序即将完成

注意

此示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter02/ReactiveSumV2.java上查看/下载。

使用 lambda 使一切变得更简单。看看前面的程序,我们可以看到大部分逻辑由小型独立函数组成,使用其他函数链接在一起。这就是我们所说的功能性,使用这样的小型可重用函数来表达我们的程序,这些函数接受其他函数并返回函数和数据抽象,使用函数链转换输入数据以产生所需的结果。但让我们深入研究这些函数。

纯函数和高阶函数

您不必记住本章介绍的大部分术语;重要的是要理解它们如何帮助我们编写简单但功能强大的程序。

RxJava 的方法融合了许多功能性思想,因此重要的是我们学会如何以更加功能性的方式思考,以便编写更好的响应式应用程序。

纯函数

纯函数是一个其返回值仅由其输入决定的函数,没有可观察的副作用。如果我们用相同的参数调用它n次,每次都会得到相同的结果。例如:

Predicate<Integer> even = (number) -> number % 2 == 0;
int i = 50;
while((i--) > 0) {
  System.out.println("Is five even? - " + even.test(5));
}

每次,偶函数返回False,因为它仅依赖于其输入,而每次输入都是相同的,甚至不是。

纯函数的这个特性称为幂等性。幂等函数不依赖于时间,因此它们可以将连续数据视为无限数据流。这就是 RxJava(Observable实例)中表示不断变化的数据的方式。

注意

请注意,在这里,“幂等性”一词是以计算机科学的意义使用的。在计算中,幂等操作是指如果使用相同的输入参数多次调用它,它不会产生额外的效果;在数学中,幂等操作是指满足这个表达式的操作:f(f(x)) = f(x)

纯函数不会产生副作用。例如:

Predicate<Integer> impureEven = (number) -> {
  System.out.println("Printing here is side effect!");
  return number % 2 == 0;
};

这个函数不是纯的,因为每次调用它时都会在输出上打印一条消息。所以它做了两件事:它测试数字是否为偶数,并且作为副作用输出一条消息。副作用是函数可以产生的任何可能的可观察输出,例如触发事件、抛出异常和 I/O,与其返回值不同。副作用还会改变共享状态或可变参数。

想想看。如果你的大部分程序由纯函数组成,它将很容易扩展,并且可以并行运行部分,因为纯函数不会相互冲突,也不会改变共享状态。

在本节中值得一提的另一件事是不可变性。不可变对象是指不能改变其状态的对象。Java 中的String类就是一个很好的例子。String实例是不可变的;即使像substring这样的方法也会创建一个新的String实例,而不会修改调用它的实例。

如果我们将不可变数据传递给纯函数,我们可以确保每次使用这些数据调用它时,它都会返回相同的结果。对于可变对象,在编写并行程序时情况就不太一样了,因为一个线程可以改变对象的状态。在这种情况下,如果调用纯函数,它将返回不同的结果,因此不再是幂等的。

如果我们将数据存储在不可变对象中,并使用纯函数对其进行操作,在此过程中创建新的不可变对象,我们将不会受到意外并发问题的影响。不会有全局状态和可变状态;一切都将简单而可预测。

使用不可变对象是棘手的;对它们的每个操作都会创建新的实例,这可能会消耗内存。但有方法可以避免这种情况;例如,尽可能多地重用源不可变对象,或使不可变对象的生命周期尽可能短(因为生命周期短的对象对 GC 或缓存友好)。函数式程序应该设计为使用不可变的无状态数据。

复杂的程序不能只由纯函数组成,但只要可能,最好使用它们。在本章对The Reactive Sum的实现中,我们只传递了纯函数给map()filter()combineLatest()

谈到map()filter()函数,我们称它们为高阶函数。

高阶函数

至少有一个函数类型参数或返回函数的函数被称为高阶函数。当然,高阶函数可以是纯的

这是一个接受函数参数的高阶函数的例子:

public static <T, R> int highSum(
  Function<T, Integer> f1,
  Function<R, Integer> f2,
  T data1,
  R data2) {
    return f1.apply(data1) + f2.apply(data2);
  }
)

它需要两个类型为T -> int/R -> int的函数和一些数据来调用它们并对它们的结果求和。例如,我们可以这样做:

highSum(v -> v * v, v -> v * v * v, 3, 2);

这里我们对三的平方和两的立方求和。

但高阶函数的理念是灵活的。例如,我们可以使用highSum()函数来完成完全不同的目的,比如对字符串求和,如下所示:

Function<String, Integer> strToInt = s -> Integer.parseInt(s);

highSum(strToInt, strToInt, "4",  "5");

因此,高阶函数可以用于将相同的行为应用于不同类型的输入。

如果我们传递给highSum()函数的前两个参数是纯函数,那么它也将是一个纯函数。strToInt参数是一个纯函数,如果我们调用highSum(strToInt, strToInt, "4", "5")方法n次,它将返回相同的结果,并且不会产生副作用。

这是另一个高阶函数的例子:

public static Function<String, String> greet(String greeting) {
  return (String name) -> greeting + " " + name + "!";
}

这是一个返回另一个函数的函数。它可以这样使用:

System.out.println(greet("Hello").apply("world"));
// Prints 'Hellow world!'

System.out.println(greet("Goodbye").apply("cruel world"));
// Prints 'Goodbye cruel world!'

Function<String, String> howdy = greet("Howdy");

System.out.println(howdy.apply("Tanya"));
System.out.println(howdy.apply("Dali"));
// These two print 'Howdy Tanya!' and 'Howdy Dali'

注意

此示例的代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter02/PureAndHigherOrderFunctions.java找到。

这些函数可以用来实现具有共同点的不同行为。在面向对象编程中,我们定义类然后扩展它们,重载它们的方法。在函数式编程中,我们将高阶函数定义为接口,并使用不同的参数调用它们,从而产生不同的行为。

这些函数是一等公民;我们可以仅使用函数编写我们的逻辑,将它们链接在一起,并处理我们的数据,将其转换、过滤或累积成一个结果。

RxJava 和函数式编程

纯函数和高阶函数等函数式概念对 RxJava 非常重要。RxJava 的Observable类是流畅接口的一种实现。这意味着它的大多数实例方法都返回一个Observable实例。例如:

Observable mapped = observable.map(someFunction);

map()操作符返回一个新的Observable实例,发出经过转换的数据。诸如map()之类的操作符显然是高阶函数,我们可以向它们传递其他函数。因此,典型的 RxJava 程序由一系列操作符链接到一个Observable实例表示,多个订阅者可以订阅它。这些链接在一起的函数可以受益于本章涵盖的主题。我们可以向它们传递 lambda 而不是匿名接口实现(就像我们在Reactive Sum的第二个实现中看到的那样),并且我们应该尽可能使用不可变数据和纯函数。这样,我们的代码将会简单且安全。

总结

在本章中,我们已经了解了一些函数式编程原则和术语。我们学会了如何编写由小的纯函数动作组成的程序,使用高阶函数链接在一起。

随着函数式编程的日益流行,精通它的开发人员将在不久的将来需求量很大。这是因为它帮助我们轻松实现可伸缩性和并行性。而且,如果我们将响应式思想加入其中,它将变得更加吸引人。

这就是为什么我们将在接下来的章节中深入研究 RxJava 框架,学习如何将其用于我们的利益。我们将从Observable实例创建技术开始。这将使我们具备从任何东西创建Observable实例的技能,从而将几乎一切转变为函数式响应式程序。

第三章:创建和连接 Observables、Observers 和 Subjects

RxJava 的 Observable 实例是响应式应用程序的构建模块,这是 RxJava 的优势。如果我们有一个源 Observable 实例,我们可以将逻辑链接到它并订阅结果。我们只需要这个初始的 Observable 实例。

在浏览器或桌面应用程序中,用户输入已经被表示为我们可以处理并通过 Observable 实例转发的事件。但是将所有数据更改或操作转换为 Observable 实例会很好,而不仅仅是用户输入。例如,当我们从文件中读取数据时,将每一行读取或每个字节序列视为可以通过 Observable 实例发出的消息将会很好。

我们将详细了解如何将不同的数据源转换为 Observable 实例;无论它们是外部的(文件或用户输入)还是内部的(集合或标量)都无关紧要。此外,我们将了解各种类型的 Observable 实例,取决于它们的行为。另一个重要的是我们将学习如何何时取消订阅 Observable 实例以及如何使用订阅和 Observer 实例。此外,我们还将介绍 Subject 类型及其用法。

在本章中,我们将学习以下内容:

  • Observable 工厂方法——justfromcreate

  • 观察者和订阅者

  • 热和冷 Observable;可连接的 Observable

  • 主题是什么以及何时使用它们

  • Observable 创建

有很多种方法可以从不同的来源创建 Observable 实例。原则上,可以使用 Observable.create(OnSubscribe<T>) 方法创建 Observable 实例,但是有许多简单的方法,旨在让我们的生活更美好。让我们来看看其中一些。

Observable.from 方法

Observable.from 方法可以从不同的 Java 结构创建 Observable 实例。例如:

List<String> list = Arrays.asList(
  "blue", "red", "green", "yellow", "orange", "cyan", "purple"
);
Observable<String> listObservable = Observable.from(list);
listObservable.subscribe(System.out::println);

这段代码从 List 实例创建了一个 Observable 实例。当在 Observable 实例上调用 subscribe 方法时,源列表中包含的所有元素都将被发射到订阅方法中。对于每次调用 subscribe() 方法,整个集合都会从头开始逐个元素发射:

listObservable.subscribe(
  color -> System.out.print(color + "|"),
  System.out::println,
  System.out::println
);
listObservable.subscribe(color -> System.out.print(color + "/"));

这将以不同的格式两次打印颜色。

这个版本的 from 方法的真正签名是 final static <T> Observable<T> from(Iterable<? extends T> iterable)。这意味着可以将实现 Iterable 接口的任何类的实例传递给这个方法。这些包括任何 Java 集合,例如:

Path resources = Paths.get("src", "main", "resources");
try (DirectoryStream<Path> dStream =Files.newDirectoryStream(resources)) {
  Observable<Path> dirObservable = Observable.from(dStream);
  dirObservable.subscribe(System.out::println);
}
catch (IOException e) {
  e.printStackTrace();
}

这将把文件夹的内容转换为我们可以订阅的事件。这是可能的,因为 DirectoryStream 参数是一个 Iterable 实例。请注意,对于此 Observable 实例的每次调用 subscribe 方法,它的 Iterable 源的 iterator() 方法都会被调用以获取一个新的 Iterator 实例,用于从头开始遍历数据。使用此示例,第二次调用 subscribe() 方法时将抛出 java.lang.IllegalStateException 异常,因为 DirectoryStream 参数的 iterator() 方法只能被调用一次。

用于从数组创建 Observable 实例的 from 方法的另一个重载是 public final static <T> Observable<T> from(T[] array),使用 Observable 实例的示例如下:

Observable<Integer> arrayObservable = Observable.from(new Integer[] {3, 5, 8});
  arrayObservable.subscribe(System.out::println);

Observable.from() 方法非常有用,可以从集合或数组创建 Observable 实例。但是有些情况下,我们需要从单个对象创建 Observable 实例;对于这些情况,可以使用 Observable.just() 方法。

注意

使用Observable.from()方法的示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/CreatingObservablesWithFrom.java中查看和下载。

Observable.just 方法

just()方法将其参数作为OnNext通知发出,然后发出OnCompleted通知。

例如,一个字母:

Observable.just('S').subscribe(System.out::println);

或者一系列字母:

Observable
  .just('R', 'x', 'J', 'a', 'v', 'a')
  .subscribe(
    System.out::print,
    System.err::println,
    System.out::println
  );

第一段代码打印S和一个换行,第二段代码打印字母并在完成时添加一个换行。该方法允许通过响应式手段观察最多九个任意值(相同类型的对象)。例如,假设我们有这个简单的User类:

public static class User {
  private final String forename;
  private final String lastname;
  public User(String forename, String lastname) {
    this.forename = forename;
    this.lastname = lastname;
  }
  public String getForename() {
    return this.forename;
  }
  public String getLastname() {
    return this.lastname;
  }
}

我们可以这样打印User实例的全名:

Observable
  .just(new User("Dali", "Bali"))
  .map(u -> u.getForename() + " " + u.getLastname())
  .subscribe(System.out::println);

这并不是非常实用,但展示了将数据放入Observable实例上下文并利用map()方法的方法。一切都可以成为一个事件。

还有一些更方便的工厂方法,可在各种情况下使用。让我们在下一节中看看它们。

注意

Observable.just()方法示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/CreatingObservablesUsingJust.java中查看/下载。

其他 Observable 工厂方法

在这里,我们将检查一些可以与转换操作符(如 flatMap)或组合操作符(如.zip文件)结合使用的方法(有关更多信息,请参见下一章)。

为了检查它们的结果,我们将使用以下方法创建订阅:

void subscribePrint(Observable<T> observable, String name) {
  observable.subscribe(
    (v) -> System.out.println(name + " : " + v),
    (e) -> {
      System.err.println("Error from " + name + ":");
      System.err.println(e.getMessage());
    },
    () -> System.out.println(name + " ended!")
  );
}

前面方法的想法是订阅一个Observable实例并用名称标记它。在OnNext时,它打印带有名称前缀的值;在OnError时,它与名称一起打印错误;在OnCompleted时,它打印带有名称前缀的'ended!'。这有助于我们调试结果。

注意

前面方法的源代码可以在github.com/meddle0x53/learning-rxjava/blob/4a2598aa0835235e6ef3bc3371a3c19896161628/src/main/java/com/packtpub/reactive/common/Helpers.java#L25找到。

以下是介绍新工厂方法的代码:

subscribePrint(
  Observable.interval(500L, TimeUnit.MILLISECONDS),
  "Interval Observable"
);
subscribePrint(
  Observable.timer(0L, 1L, TimeUnit.SECONDS),
  "Timed Interval Observable"
);
subscribePrint(
  Observable.timer(1L, TimeUnit.SECONDS),
  "Timer Observable"
);

subscribePrint(
  Observable.error(new Exception("Test Error!")),
  "Error Observable"
);
subscribePrint(Observable.empty(), "Empty Observable");
subscribePrint(Observable.never(), "Never Observable");
subscribePrint(Observable.range(1, 3), "Range Observable");
Thread.sleep(2000L);

以下是代码中发生的情况:

  • Observable<Long> Observable.interval(long, TimeUnit, [Scheduler]):此方法创建一个Observable实例,将以给定间隔发出顺序数字。它可用于实现周期性轮询,或通过仅忽略发出的数字并发出有用消息来实现连续状态记录。该方法的特殊之处在于,默认情况下在计算线程上运行。我们可以通过向方法传递第三个参数——Scheduler实例(有关Scheduler实例的更多信息,请参见第六章, 使用调度程序进行并发和并行处理)来更改这一点。

  • Observable<Long> Observable.timer(long, long, TimeUnit, [Scheduler])interval()方法仅在等待指定时间间隔后开始发出数字。如果我们想要告诉它在何时开始工作,可以使用此timer()方法。它的第一个参数是开始时间,第二个和第三个是间隔设置。同样,默认情况下在计算线程上执行,同样,这是可配置的。

  • Observable<Long> Observable.timer(long, TimeUnit, [Scheduler]):这个在计算线程(默认情况下)上在一定时间后只发出输出'0'。之后,它发出一个completed通知。

  • <T> Observable<T> Observable.error(Throwable):这只会将传递给它的错误作为OnError通知发出。这类似于经典的命令式 Java 世界中的throw关键字。

  • <T> Observable<T> Observable.empty():这个不发出任何项目,但立即发出一个OnCompleted通知。

  • <T> Observable<T> Observable.never():这个什么都不做。它不向其Observer实例发送任何通知,甚至OnCompleted通知也不发送。

  • Observable<Integer> Observable.range(int, int, [Scheduler]):此方法从传递的第一个参数开始发送顺序数字。第二个参数是发射的数量。

这个程序将打印以下输出:

Timed Interval Observable : 0
Error from Error Observable:
Test Error!
Range Observable : 1
Range Observable : 2
Range Observable : 3
Range Observable ended!
Empty Observable ended!
Interval Observable : 0
Interval Observable : 1
Timed Interval Observable : 1
Timer Observable : 0
Timer Observable ended!
Interval Observable : 2
Interval Observable : 3
Timed Interval Observable : 2

正如你所看到的,interval Observable实例不会发送OnCompleted通知。程序在两秒后结束,interval Observable实例在 500 毫秒后开始发出,每 500 毫秒发出一次;因此,它发出了三个OnNext通知。timed interval Observable实例在创建后立即开始发出,每秒发出一次;因此,我们从中得到了两个通知。

注意

前面示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/CreatingObservablesUsingVariousFactoryMethods.java上查看/下载。

所有这些方法都是使用Observable.create()方法实现的。

Observable.create 方法

让我们首先看一下该方法的签名:

public final static <T> Observable<T> create(OnSubscribe<T>)

它接受一个OnSubscribe类型的参数。这个接口扩展了Action1<Subscriber<? super T>>接口;换句话说,这种类型只有一个方法,接受一个Subscriber<T>类型的参数并返回空。每次调用Observable.subscribe()方法时,都会调用此函数。它的参数,Subscriber类的一个实例,实际上是观察者,订阅Observable实例(这里,Subscriber类和 Observer 接口扮演相同的角色)。我们将在本章后面讨论它们。我们可以在其上调用onNext()onError()onCompleted()方法,实现我们自己的自定义行为。

通过一个例子更容易理解。让我们实现Observable.from(Iterabale<T>)方法的一个简单版本:

<T> Observable<T> fromIterable(final Iterable<T> iterable) {
  return Observable.create(new OnSubscribe<T>() {
    @Override
    public void call(Subscriber<? super T> subscriber) {
      try {
        Iterator<T> iterator = iterable.iterator(); // (1)
        while (iterator.hasNext()) { // (2)
          subscriber.onNext(iterator.next());
        }
        subscriber.onCompleted(); // (3)
      }
      catch (Exception e) {
        subscriber.onError(e); // (4)
      }
    }
  });
}

该方法以一个Iterable<T>参数作为参数,并返回一个Observable<T>参数。行为如下:

  1. 当一个Observer/Subscriber实例订阅生成的Observable实例时,会从Iterable源中检索一个Iterator实例。Subscriber类实际上实现了Observer接口。它是一个抽象类,on*方法不是由它实现的。

  2. 当有元素时,它们作为OnNext通知被发送。

  3. 当所有元素都被发出时,将发送一个OnCompleted通知。

  4. 如果在任何时候发生错误,将会发送一个OnError通知与错误。

这是Observable.from(Iterable<T>)方法行为的一个非常简单和天真的实现。第一章和第二章中描述的 Reactive Sum 是Observable.create方法的另一个例子(由CreateObservable.from()使用)。

但正如我们所看到的,传递给create()方法的逻辑是在Observable.subscribe()方法在Observable实例上被调用时触发的。到目前为止,我们一直在创建Observable实例并使用这种方法订阅它们。现在是时候仔细看一下了。

订阅和取消订阅

Observable.subscribe()方法有许多重载,如下所示:

  • subscribe(): 这个方法忽略来自 Observable 实例的所有发射,并且如果有 OnError 通知,则抛出一个 OnErrorNotImplementedException 异常。这可以用来触发OnSubscribe.call行为。

  • subscribe(Action1<? super T>): 这只订阅onNext()方法触发的更新。它忽略OnCompleted通知,并且如果有OnError通知,则抛出一个 OnErrorNotImplementedException 异常。这不是真正的生产代码的好选择,因为很难保证不会抛出错误。

  • subscribe(Action1<? super T>, Action1<Throwable>): 这与前一个方法相同,但如果有OnError通知,则调用第二个参数。

  • subscribe(Action1<? super T>,Action1<Throwable>, Action0): 这与前一个方法相同,但第三个参数在OnCompleted通知时被调用。

  • subscribe(Observer<? super T>): 这使用其 Observer 参数的onNext/onError/onCompleted方法来观察 Observable 实例发出的通知。我们在第一章中实现"响应式求和"时使用了这个方法。

  • subscribe(Subscriber<? super T>): 这与前一个方法相同,但使用 Observer 接口的 Subscriber 实现来观察通知。Subscriber 类提供了高级功能,如取消订阅(取消)和背压(流量控制)。实际上,所有前面的方法都调用这个方法;这就是为什么我们从现在开始谈论Observable.subscribe时将引用它。该方法确保传递的 Subscriber 实例看到一个 Observable 实例,符合以下Rx contract

"发送到 Observer 接口实例的消息遵循以下语法:

onNext (onCompleted | onError)?*

这种语法允许可观察序列向 Subscriber 发送任意数量(0 个或更多个)的OnNext()方法消息,可选地跟随单个成功(onCompleted)或失败(onError)消息。指示可观察序列已完成的单个消息确保可观察序列的消费者可以确定地建立安全执行清理操作。单个失败进一步确保可以维护对多个可观察序列进行操作的操作符的中止语义。

  • RxJava 的 JavaDoc 的一部分。

这是通过在传递的 Subscriber 实例周围使用一个包装器——SafeSubscriber 来内部完成的。

  • unsafeSubscribe(Subscriber<? super T>): 这与前一个方法相同,但没有Rx contract保护。它旨在帮助实现自定义操作符(参见第八章,“资源管理和扩展 RxJava”),而不会增加subscribe()方法的额外开销;在一般代码中使用这种方法观察 Observable 实例是不鼓励的。

所有这些方法返回 Subscription 类型的结果,可以用于从 Observable 实例发出的通知中取消订阅。取消订阅通常会清理与订阅相关的内部资源;例如,如果我们使用Observable.create()方法实现一个 HTTP 请求,并希望在特定时间取消它,或者我们有一个发射无限序列的数字/单词/任意数据的 Observable 实例,并希望停止它。

Subscription 接口有两个方法:

  • void unsubscribe(): 这用于取消订阅

  • boolean isUnsubscribed(): 这用于检查 Subscription 实例是否已经取消订阅

传递给Observable.create()方法的OnSubscribe()方法的Subscriber类的实例实现了Subscription接口。因此,在编写Observable实例的行为时,可以进行取消订阅和检查Subscriber是否已订阅。让我们更新我们的Observable<T> fromIterable(Iterable<T>)方法的实现以对取消订阅做出反应:

<T> Observable<T> fromIterable(final Iterable<T> iterable) {
  return Observable.create(new OnSubscribe<T>() {
    @Override
    public void call(Subscriber<? super T> subscriber) {
      try {
        Iterator<T> iterator = iterable.iterator();
        while (iterator.hasNext()) {
          if (subscriber.isUnsubscribed()) {
 return;
 }
          subscriber.onNext(iterator.next());
        }
        if (!subscriber.isUnsubscribed()) {
 subscriber.onCompleted();
 }
 }
 catch (Exception e) {
 if (!subscriber.isUnsubscribed()) {
 subscriber.onError(e);
 }
 }
    }
  });
}

新的地方在于Subscription.isUnsubscribed()方法用于确定是否应终止数据发射。我们在每次迭代时检查Subscriber是否已取消订阅,因为它可以随时取消订阅,之后我们将不需要再发出任何内容。在发出所有内容之后,如果 Subscriber 已经取消订阅,则会跳过onCompleted()方法。如果有异常,则只有在Subscriber实例仍然订阅时才会作为OnError通知发出。

让我们看看取消订阅是如何工作的:

Path path = Paths.get("src", "main", "resources", "lorem_big.txt"); // (1)
List<String> data = Files.readAllLines(path);
Observable<String> observable = fromIterable(data).subscribeOn(Schedulers.computation()); // (2)
Subscription subscription = subscribePrint(observable, "File");// (3)
System.out.println("Before unsubscribe!");
System.out.println("-------------------");
subscription.unsubscribe(); // (4)
System.out.println("-------------------");
System.out.println("After unsubscribe!");

以下是这个例子中发生的事情:

  1. 数据源是一个巨大的文件,因为我们需要一些需要一些时间来迭代的东西。

  2. Observable实例的所有订阅将在另一个线程上进行,因为我们希望在主线程上取消订阅

  3. 在本章中定义的subscribePrint()方法被使用,但已修改为返回Subscription

  4. 订阅用于从Observable实例取消订阅,因此整个文件不会被打印,并且会显示取消订阅执行的标记。

输出将类似于这样:

File : Donec facilisis sollicitudin est non molestie.
File : Integer nec magna ac ex rhoncus imperdiet.
Before unsubscribe!
-------------------
File : Nullam pharetra iaculis sem.
-------------------
After unsubscribe!

大部分文件内容被跳过。请注意,可能会在取消订阅后立即发出某些内容;例如,如果Subscriber实例在检查取消订阅后立即取消订阅,并且程序已经执行if语句的主体,则会发出内容。

注意

前面示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/ObservableCreateExample.java中下载/查看。

还要注意的一点是,Subscriber实例有一个void add(Subscription s)方法。当Subscriber取消订阅时,传递给它的每个订阅将自动取消订阅。这样,我们可以向Subscriber实例添加额外的操作;例如,在取消订阅时应执行的操作(类似于 Java 中的 try-finally 结构)。这就是取消订阅的工作原理。在第八章中,我们将处理资源管理。我们将学习如何通过Subscription包装器将Observable实例附加到Subscriber实例,并且调用取消订阅将释放任何分配的资源。

在本章中,我们将讨论与订阅行为相关的下一个主题。我们将谈论热和冷的Observable实例。

热和冷的 Observable 实例

查看使用Observable.create()Observable.just()Observable.from()方法实现的先前示例时,我们可以说在有人订阅它们之前,它们是不活动的,不会发出任何内容。但是,每次有人订阅时,它们就开始发出它们的通知。例如,如果我们对Observable.from(Iterable)对象进行三次订阅,Iterable实例将被迭代次。像这样行为的Observable实例被称为冷的 Observable 实例。

在本章中我们一直在使用的所有工厂方法返回冷的 Observables。冷的 Observables 按需产生通知,并且对于每个 Subscriber,它们产生独立的通知。

有些Observable实例在开始发出通知时,无论是否有订阅,都会继续发出通知,直到完成。所有订阅者都会收到相同的通知,默认情况下,当一个订阅者订阅时,它不会收到之前发出的通知。这些是热 Observable 实例。

我们可以说,冷 Observables 为每个订阅者生成通知,而热 Observables 始终在运行,向所有订阅者广播通知。把热 Observable 想象成一个广播电台。此刻收听它的所有听众都在听同一首歌。冷 Observable 就像一张音乐 CD。许多人可以购买并独立听取它。

正如我们提到的,本书中有很多使用冷 Observables 的例子。那么热 Observable 实例呢?如果你还记得我们在第一章中实现'响应式求和'时,我们有一个Observable实例,它会发出用户在标准输入流中输入的每一行。这个是热的,并且我们从中派生了两个Observable实例,一个用于收集器a,一个用于b。它们接收相同的输入行,并且只过滤出它们感兴趣的行。这个输入Observable实例是使用一种特殊类型的Observable实现的,称为ConnectableObservable

ConnectableObservable 类

这些Observable实例在调用它们的connect()方法之前是不活跃的。之后,它们就变成了热 Observables。可以通过调用其publish()方法从任何Observable实例创建ConnectableObservable实例。换句话说,publish()方法可以将任何冷 Observable 转换为热 Observable。让我们看一个例子:

Observable<Long> interval = Observable.interval(100L, TimeUnit.MILLISECONDS);
ConnectableObservable<Long> published = interval.publish();
Subscription sub1 = subscribePrint(published, "First");
Subscription sub2 = subscribePrint(published, "Second");
published.connect();
Subscription sub3 = null;
try {
  Thread.sleep(500L);
  sub3 = subscribePrint(published, "Third");
  Thread.sleep(500L);
}
catch (InterruptedException e) {}
sub1.unsubscribe();
sub2.unsubscribe();
sub3.unsubscribe();

在调用connect()方法之前什么都不会发生。之后,我们将看到相同的顺序数字输出两次——每个订阅者一次。第三个订阅者将加入其他两个,打印在第一个 500 毫秒后发出的数字,但它不会打印其订阅之前发出的数字。

如果我们想要在我们的订阅之前接收所有已发出的通知,然后继续接收即将到来的通知,可以通过调用replay()方法而不是publish()方法来实现。它从源Observable实例创建一个ConnectableObservable实例,有一个小变化:所有订阅者在订阅时都会收到所有通知(之前的通知将按顺序同步到达)。

有一种方法可以激活Observable实例,使其在不调用connect()方法的情况下变为热 Observable。它可以在第一次订阅时激活,并在每个Subscriber实例取消订阅时停用。可以通过在ConnectableObservable实例上调用refCount()方法(方法的名称来自'引用计数';它计算订阅到由它创建的Observable实例的Subscriber实例数量)从ConnectableObservable实例创建这样的Observable实例。以下是使用refCount()方法实现的前面的例子:

Observable<Long> refCount = interval.publish().refCount();
Subscription sub1 = subscribePrint(refCount, "First");
Subscription sub2 = subscribePrint(refCount, "Second");
try {
  Thread.sleep(300L);
}
catch (InterruptedException e) {}
sub1.unsubscribe();
sub2.unsubscribe();
Subscription sub3 = subscribePrint(refCount, "Third");
try {
  Thread.sleep(300L);
}
catch (InterruptedException e) { }
sub3.unsubscribe();

sub2 取消订阅后,Observable实例将停用。如果此后有人订阅它,它将从头开始发出序列。这就是sub3的情况。还有一个share()方法,它是publish().refCount()调用的别名。

注意

前面例子的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/UsingConnectableObservables.java上查看/下载。

还有一种创建热 Observable 的方法:使用Subject实例。我们将在本章的下一节和最后一节介绍它们。

Subject 实例

Subject实例既是Observable实例又是Observer实例。与Observable实例一样,它们可以有多个Observer实例,接收相同的通知。这就是为什么它们可以用来将冷的Observable实例转换为热的实例。与Observer实例一样,它们让我们访问它们的onNext()onError()onCompleted()方法。

让我们看一下使用Subject实例实现前面的热间隔示例:

Observable<Long> interval = Observable.interval(100L, TimeUnit.MILLISECONDS); // (1)
Subject<Long, Long> publishSubject = PublishSubject.create(); // (2)
interval.subscribe(publishSubject);
// (3)
Subscription sub1 = subscribePrint(publishSubject, "First");
Subscription sub2 = subscribePrint(publishSubject, "Second");
Subscription sub3 = null;
try {
  Thread.sleep(300L);
  publishSubject.onNext(555L); // (4)
  sub3 = subscribePrint(publishSubject, "Third"); // (5)
  Thread.sleep(500L);
}
catch (InterruptedException e) {}
sub1.unsubscribe(); // (6)
sub2.unsubscribe();
sub3.unsubscribe();

现在示例略有不同:

  1. 间隔Observable实例的创建方式与以前相同。

  2. 在这里,我们创建了一个PublishSubject实例 - 一个Subject实例,只向订阅后由源Observable实例发出的项目发出。这种行为类似于使用publish()方法创建的ConnectableObservable实例。新的Subject实例订阅了由间隔工厂方法创建的间隔Observable实例,这是可能的,因为Subject类实现了Observer接口。还要注意,Subject签名有两种泛型类型 - 一种是Subject实例将接收的通知类型,另一种是它将发出的通知类型。PublishSubject类的输入和输出通知类型相同。

请注意,可以创建一个PublishSubject实例而不订阅源Observable实例。它只会发出传递给其onNext()onError()方法的通知,并在调用其onCompleted()方法时完成。

  1. 我们可以订阅Subject实例;毕竟它是一个Observable实例。

  2. 我们可以随时发出自定义通知。它将广播给主题的所有订阅者。我们甚至可以调用onCompleted()方法并关闭通知流。

  3. 第三个订阅者只会收到订阅后发出的通知。

  4. 当一切都取消订阅时,Subject实例将继续发出。

注意

此示例的源代码可在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/SubjectsDemonstration.java上查看/下载。

RxJava 有四种类型的主题:

  • PublishSubject:这是我们在前面的示例中看到的,行为类似于使用publish()方法创建的ConnectableObservable

  • ReplaySubject:这会向任何观察者发出源Observable实例发出的所有项目,无论观察者何时订阅。因此,它的行为类似于使用replay()方法创建的ConnectableObservableReplaySubject类有许多工厂方法。默认的工厂方法会缓存所有内容;请记住这一点,因为它可能会占用内存。有用于使用大小限制和/或时间限制缓冲区创建它的工厂方法。与PublishSubject类一样,这个可以在没有源Observable实例的情况下使用。使用其onNext()onError()onCompleted()方法发出的所有通知都将发送给每个订阅者,即使在调用on*方法后订阅。

  • BehaviorSubject:当观察者订阅它时,它会发出源Observable实例最近发出的项目(如果尚未发出任何项目,则发出种子/默认值),然后继续发出源Observable实例后来发出的任何其他项目。BehaviorSubject类几乎与具有缓冲区大小为一的ReplaySubjects类相似。BehaviorSubject类可用于实现有状态的响应实例 - 一个响应属性。再次强调,不需要源Observable实例。

  • AsyncSubject:这会发出源Observable实例发出的最后一个值(仅此一个),并且只有在源Observable实例完成后才会发出。如果源Observable实例没有发出任何值,AsyncSubject实例也会在不发出任何值的情况下完成。这在 RxJava 的世界中有点像promise。不需要源Observable实例;可以通过调用on*方法将值、错误或OnCompleted通知传递给它。

使用主题可能看起来是解决各种问题的一种很酷的方式,但你应该避免使用它们。或者,至少要在返回Observable类型的结果的方法中实现它们和它们的行为。

Subject实例的危险在于它们提供了onNext()onError()onCompleted()方法的访问权限,你的逻辑可能会变得混乱(它们需要遵循本章前面引用的 Rx 合同)。它们很容易被滥用。

在需要从冷 Observable 创建热 Observable 时,选择使用ConnecatableObservable实例(即通过publish()方法)而不是Subject

但让我们看一个Subject实例的一个很好的用法——前面提到的反应性属性。同样,我们将实现'The Reactive Sum',但这次会有很大不同。以下是定义它的类:

public class ReactiveSum { // (1)
  private BehaviorSubject<Double> a = BehaviorSubject.create(0.0);
 private BehaviorSubject<Double> b = BehaviorSubject.create(0.0);
 private BehaviorSubject<Double> c = BehaviorSubject.create(0.0);
  public ReactiveSum() { // (2)
    Observable.combineLatest(a, b, (x, y) -> x + y).subscribe(c);
  }
  public double getA() { // (3)
    return a.getValue();
  }
  public void setA(double a) {
    this.a.onNext(a);
  }
  public double getB() {
    return b.getValue();
  }
  public void setB(double b) {
    this.b.onNext(b);
  }
  public double getC() { // (4)
    return c.getValue();
  }
  public Observable<Double> obsC() {
    return c.asObservable();
  }
}

这个类有三个双精度属性:两个可设置的属性ab,以及它们的c。当ab改变时,c自动更新为它们的和。我们可以使用一种特殊的方法来跟踪c的变化。那它是如何工作的呢?

  1. ReactiveSum是一个普通的 Java 类,定义了三个BehaviorSubject<Double>类型的私有字段,表示变量abc,默认值为零。

  2. 在构造函数中,我们订阅c依赖于ab,并且等于它们的和,再次使用combineLatest()方法。

  3. 属性ab有 getter 和 setter。getter 返回它们当前的值——最后接收到的值。setter 将传递的值发出到它们的Subject实例,使其成为最后一个。

注意

BehaviorSubject参数的getValue()方法用于检索它。它在 RxJava 1.0.5 中可用。

  1. 属性c是只读的,所以它只有一个 getter,但可以被监听。这可以通过obsC()方法来实现,它将其作为Observable实例返回。记住,当你使用主题时,要始终将它们封装在类型或方法中,并将可观察对象返回给外部世界。

这个ReactiveSum类可以这样使用:

ReactiveSum sum = new ReactiveSum();
subscribePrint(sum.obsC(), "Sum");
sum.setA(5);
sum.setB(4);

这将输出以下内容:

Sum : 0.0
Sum : 5.0
Sum : 9.0

第一个值在subscribe ()方法上发出(记住BehaviorSubject实例总是在订阅时发出它们的最后一个值),其他两个将在设置ab时自动发出

注意

前面示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter03/ReactiveSumV3.java上查看/下载。

反应性属性可用于实现绑定和计数器,因此它们对于桌面或浏览器应用程序非常有用。但这个例子远离了任何功能范式。

总结

在本章中,我们学习了许多创建不同类型的Observable实例和其他相关实例(ObserverSubscriberSubscriptionSubject)的方法。我们已经从计时器,值,集合和文件等外部来源创建了它们。利用这些知识作为基础,我们可以开始通过对它们进行操作来构建逻辑。这里介绍的许多工厂方法将在接下来的章节中再次出现。例如,我们将使用Observable.create方法构建不同的行为。

在下一章中,我们将介绍各种操作符,这将赋予我们使用Observable实例编写真正逻辑的能力。我们已经提到了其中一些,比如map()filter(),但现在是时候深入研究它们了。

第四章:转换、过滤和累积您的数据

现在我们有了从各种来源数据创建Observable实例的手段,是时候围绕这些实例构建编程逻辑了。我们将介绍基本的响应式操作符,用于逐步计算(处理数据的响应式方式)。

我们将从转换开始,使用著名的flatMap()map()操作符,以及一些不太常见的转换操作符。之后,我们将学习如何使用filter()操作符过滤我们的数据,跳过元素,仅在给定时间位置接收元素。本章还将涵盖使用scan操作符累积数据。大多数这些操作符将使用大理石图示进行演示。

本章涵盖以下主题:

  • 大理石图示和映射转换的介绍

  • 过滤您的数据

  • 使用scan操作符累积值

Observable 转换

我们在一些先前的示例中使用了map()操作符。将传入的值转换为其他内容的高阶函数称为转换。可以在Observable实例上调用的高阶函数,从中产生新的Observable实例的操作符称为操作符。转换操作符以某种方式转换从源Observable实例发出的元素。

为了理解不同的操作符是如何工作的,我们将使用称为大理石图示的图片。例如,这个描述了map操作符:

Observable 转换

图示中心的矩形代表操作符(函数)。它将其输入(圆圈)转换为其他东西(三角形)。矩形上方的箭头代表源Observable实例,上面的彩色圆圈代表时间发出的OnNext 通知,末端的垂直线是OnCompleted 通知。矩形下方的箭头是具有其转换元素的Observable实例的输出。

因此,map()操作符确切地做到了这一点:它将源的每个'next'值转换为通过传递给它的函数定义的其他内容。这里有一个小例子:

Observable<String> mapped = Observable
  .just(2, 3, 5, 8)
  .map(v -> v * 3)
  .map(v -> (v % 2 == 0) ? "even" : "odd");
subscribePrint(mapped, "map");

第一个map()操作符将源发出的每个数字转换为它本身乘以三。第二个map()操作符将每个乘数转换为一个字符串。如果数字是偶数,则字符串是'even',否则是'odd'。

使用map()操作符,我们可以将每个发出的值转换为一个新值。还有更强大的转换操作符,看起来类似于map()操作符,但具有自己的用途和目的。让我们来看看它们。

使用各种 flatMap 操作符进行转换

flatMap操作符就像map()操作符,但有两个不同之处:

  • flatMap操作符的参数不是接收将值转换为任意类型值的函数,而是始终将值或值序列转换为Observable实例的形式。

  • 它合并了由这些结果Observable实例发出的值。这意味着它不是将Observable实例作为值发出,而是发出它们的通知。

这是它的大理石图示:

使用各种 flatMap 操作符进行转换

正如我们所看到的,源Observable实例的每个值都被转换为一个Observable实例,最终,所有这些派生 Observable的值都由结果Observable实例发出。请注意,结果Observable实例可能以交错的方式甚至无序地发出派生Observable实例的值。

flatMap运算符对于分叉逻辑非常有用。例如,如果一个Observable实例表示文件系统文件夹并从中发出文件,我们可以使用flatMap运算符将每个文件对象转换为一个Observable实例,并对这些文件 observables应用一些操作。结果将是这些操作的摘要。以下是一个从文件夹中读取一些文件并将它们转储到标准输出的示例:

Observable<Path> listFolder(Path dir, String glob) { // (1)
  return Observable.<Path>create(subscriber -> {
    try {
      DirectoryStream<Path> stream = Files.newDirectoryStream(dir, glob);
      subscriber.add(Subscriptions.create(() -> {
        try {
          stream.close();
        }
        catch (IOException e) {
          e.printStackTrace();
        }
      }));
      Observable.<Path>from(stream).subscribe(subscriber);
    }
    catch (DirectoryIteratorException ex) {
      subscriber.onError(ex);
    }
    catch (IOException ioe) {
      subscriber.onError(ioe);
    }
  });
}
Observable<String> from(final Path path) { // (2)
  return Observable.<String>create(subscriber -> {
    try {
      BufferedReader reader = Files.newBufferedReader(path);
      subscriber.add(Subscriptions.create(() -> {
        try {
          reader.close();
        }
        catch (IOException e) {
          e.printStackTrace();
        }
      }));
      String line = null;
      while ((line = reader.readLine()) != null && !subscriber.isUnsubscribed()) {
        subscriber.onNext(line);
      }
      if (!subscriber.isUnsubscribed()) {
        subscriber.onCompleted();
      }
    }
    catch (IOException ioe) {
      if (!subscriber.isUnsubscribed()) {
        subscriber.onError(ioe);
      }
    }
  });
}
Observable<String> fsObs = listFolder(
  Paths.get("src", "main", "resources"), "{lorem.txt,letters.txt}"
).flatMap(path -> from(path)); // (3)
subscribePrint(fsObs, "FS"); // (4)

这段代码介绍了处理文件夹和文件的两种方法。我们将简要介绍它们以及在这个flatMap示例中如何使用它们:

  1. 第一个方法listFolder()接受一个Path变量形式的文件夹和一个glob表达式。它返回一个代表这个文件夹的Observable实例。这个Observable实例发出符合glob表达式的所有文件作为Path对象。

该方法使用了Observable.create()Observable.from()运算符。这个实现的主要思想是,如果发生异常,它应该被处理并由生成的Observable实例发出。

注意使用Subscriber.add()运算符将一个新的Subscription实例添加到订阅者,使用Subscriptions.create()运算符创建。这个方法使用一个动作创建一个Subscription实例。当Subscription实例被取消订阅时,这个动作将被执行,这意味着在这种情况下Subscriber实例被取消订阅。因此,这类似于将stream的关闭放在最终块中。

  1. 这个示例介绍的另一种方法是Observable<String> from(Path)

它逐行读取位于path实例中的文件并将行作为OnNext() 通知发出。该方法在Subscription实例上使用Subscriber.add()运算符来关闭到文件的stream

  1. 使用flatMap的示例从文件夹创建了一个Observable实例,使用listFolder()运算符,它发出两个Path参数到文件。对于每个文件使用flatMap()运算符,我们创建了一个Observable实例,使用from(Path)运算符,它将文件内容作为行发出。

  2. 前述链的结果将是两个文件内容,打印在标准输出上。如果我们对每个文件路径 Observable使用Scheduler实例(参见第六章, 使用调度程序进行并发和并行处理),内容将会混乱,因为flatMap运算符会交错合并Observable实例的通知。

注意

介绍Observable<String> from(final Path path)方法的源代码可以在github.com/meddle0x53/learning-rxjava/blob/724eadf5b0db988b185f8d86006d772286037625/src/main/java/com/packtpub/reactive/common/CreateObservable.java#L61找到。

包含Observable<Path> listFolder(Path dir, String glob)方法的源代码可以在github.com/meddle0x53/learning-rxjava/blob/724eadf5b0db988b185f8d86006d772286037625/src/main/java/com/packtpub/reactive/common/CreateObservable.java#L128上查看/下载。

使用flatMap运算符的示例可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter04/FlatMapAndFiles.java上查看/下载。

flatMap操作符有多个重载。例如,有一个接受三个函数的重载——一个用于OnNext,一个用于OnError,一个用于OnComleted。它还将错误完成事件转换为Observable实例,如果有OnErrorOnCompleted事件,则它们的Observable实例转换将合并到生成的Observable实例中,然后是一个OnCompleted 通知。这是一个例子:

Observable<Integer> flatMapped = Observable
  .just(-1, 0, 1)
  .map(v -> 2 / v)
  .flatMap(
 v -> Observable.just(v),
 e -> Observable.just(0),
 () -> Observable.just(42)
 );
subscribePrint(flatMapped, "flatMap");

这将输出-2(2/-1)0(因为2/0引发了错误)。由于错误1不会被发出,也不会到达flatMap操作符。

另一个有趣的重载是Observable<R> flatMap(Func1<T, Observable<U>>, Func2<T, U, R>)。这是它的弹珠图:

各种 flatMap 操作符的转换

这个操作符将源Observable实例的项目与由这些源项目触发的Observable实例的项目组合,并调用用户提供的函数,该函数使用原始和派生项目的对。然后Observable实例将发出此函数的结果。这是一个例子:

Observable<Integer> flatMapped = Observable
.just(5, 432)
.flatMap(
 v -> Observable.range(v, 2),
 (x, y) -> x + y);
subscribePrint(flatMapped, "flatMap");

输出是:

flatMap : 10
flatMap : 11
flatMap : 864
flatMap : 865
flatMap ended!

这是因为源Observable实例发出的第一个元素是5flatMap操作符使用range()操作符将其转换为Observable实例,该实例发出56。但是这个flatMap操作符并不止于此;对于这个范围Observable实例发出的每个项目,它都应用第二个函数,第一个参数是原始项目(5),第二个参数是范围发出的项目。所以我们有5 + 5,然后5 + 6。对于源Observable实例发出的第二个项目也是一样:432。它被转换为432 + 432 = 864432 + 433 = 865

当所有派生项都需要访问其源项时,这种重载是有用的,并且通常可以避免使用某种元组类,从而节省内存和库依赖。在前面的文件示例中,我们可以在每个输出行之前添加文件的名称:

CreateObservable.listFolder(
  Paths.get("src", "main", "resources"),
  "{lorem.txt,letters.txt}"
).flatMap(
 path -> CreateObservable.from(path),
 (path, line) -> path.getFileName() + " : " + line
);

flatMapIterable操作符不以 lambda 作为参数,该 lambda 以任意值作为参数并返回Observable实例。相反,传递给它的 lambda 以任意值作为参数并返回Iterable实例。所有这些Iterable实例都被展平为由生成的Observable实例发出的值。让我们看一下以下代码片段:

Observable<?> fIterableMapped = Observable
.just(
  Arrays.asList(2, 4),
  Arrays.asList("two", "four"),
)
.flatMapIterable(l -> l);

这个简单的例子合并了源Observable实例发出的两个列表,结果发出了四个项目。值得一提的是,调用flatMapIterable(list -> list)等同于调用flatMap(l → Observable.from(l))

flatMap操作符的另一种形式是concatMap操作符。它的行为与原始的flatMap操作符相同,只是它连接而不是合并生成的Observable实例,以生成自己的序列。以下弹珠图显示了它的工作原理:

各种 flatMap 操作符的转换

来自不同派生 Observable的项目不会交错,就像flatMap操作符一样。flatMapconcatMap操作符之间的一个重要区别是,flatMap操作符并行使用内部Observable实例,而concatMap操作符一次只订阅一个Observable实例。

类似于flatMap的最后一个操作符是switchMap。它的弹珠图看起来像这样:

各种 flatMap 操作符的转换

它的操作方式类似于flatMap操作符,不同之处在于每当源Observable实例发出新项时,它就会停止镜像先前发出的项生成的Observable实例,并且只开始镜像当前的Observable实例。换句话说,当下一个Observable实例开始发出其项时,它会在内部取消订阅当前的派生Observable实例。这是一个例子:

Observable<Object> obs = Observable
.interval(40L, TimeUnit.MILLISECONDS)
.switchMap(v ->
 Observable
 .timer(0L, 10L, TimeUnit.MILLISECONDS)
 .map(u -> "Observable <" + (v + 1) + "> : " + (v + u)))
);
subscribePrint(obs, "switchMap");

Observable实例使用Observable.interval()操作符每 40 毫秒发出一个连续的数字(从零开始)。使用switchMap操作符,为每个数字创建一个发出另一个数字序列的新Observable实例。这个次要数字序列从传递给switchMap操作符的源数字开始(通过使用map()操作符将源数字与每个发出的数字相加来实现)。因此,每 40 毫秒,都会发出一个新的数字序列(每个数字间隔 10 毫秒)。

结果输出如下:

switchMap : Observable <1> : 0
switchMap : Observable <1> : 1
switchMap : Observable <1> : 2
switchMap : Observable <1> : 3
switchMap : Observable <2> : 1
switchMap : Observable <2> : 2
switchMap : Observable <2> : 3
switchMap : Observable <2> : 4
switchMap : Observable <3> : 2
switchMap : Observable <3> : 3
switchMap : Observable <3> : 4
switchMap : Observable <3> : 5
switchMap : Observable <3> : 6
switchMap : Observable <4> : 3
.................

注意

所有映射示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter04/MappingExamples.java下载/查看。

分组项目

可以按特定属性或键对项目进行分组。

首先,我们来看一下groupBy()操作符,这是一个将源Observable实例分成多个Observable实例的方法。这些Observable实例根据分组函数发出源的一些项。

groupBy()操作符返回一个发出Observable实例的Observable实例。这些Observable实例很特殊;它们是GroupedObservable类型的,您可以使用getKey()方法检索它们的分组键。一旦使用groupBy()操作符,不同的组可以以不同或相同的方式处理。

请注意,当groupBy()操作符创建发出GroupedObservables实例的可观察对象时,每个实例都会缓冲其项。因此,如果我们忽略其中任何一个,这个缓冲区将会造成潜在的内存泄漏。

groupBy()操作符的弹珠图如下:

分组项目

这里,项目的形式被用作分组的共同特征。为了更好地理解这个方法的思想,我们可以看看这个例子:

List<String> albums = Arrays.asList(
  "The Piper at the Gates of Dawn",
  "A Saucerful of Secrets",
  "More", "Ummagumma",	"Atom Heart Mother",
  "Meddle", "Obscured by Clouds",
  "The Dark Side of the Moon",
  "Wish You Were Here", "Animals", "The Wall"
);
Observable
  .from(albums)
  .groupBy(album -> album.split(" ").length)
  .subscribe(obs ->
    subscribePrint(obs, obs.getKey() + " word(s)")
  );

该示例发出了一些 Pink Floyd 的专辑标题,并根据其中包含的单词数进行分组。例如,MeddleMore在键为1的同一组中,A Saucerful of SecretsWish You Were Here都在键为4的组中。所有这些组都由GroupedObservable实例表示,因此我们可以在源Observable实例的subscribe()调用中订阅它们。不同的组根据它们的键打印不同的标签。这个小程序的输出如下:

7 word(s) : The Piper at the Gates of Dawn
4 word(s) : A Saucerful of Secrets
1 word(s) : More
1 word(s) : Ummagumma
3 word(s) : Atom Heart Mother
1 word(s) : Meddle
3 word(s) : Obscured by Clouds
6 word(s) : The Dark Side of the Moon
4 word(s) : Wish You Were Here
1 word(s) : Animals
2 word(s) : The Wall

发出的项目的顺序是相同的,但它们是由不同的GroupedObservable实例发出的。此外,所有GroupedObservable实例在源完成后都会完成。

groupBy()操作符还有另一个重载,它接受第二个转换函数,以某种方式转换组中的每个项目。这是一个例子:

Observable
.from(albums)
.groupBy(
 album -> album.replaceAll("[^mM]", "").length(),
 album -> album.replaceAll("[mM]", "*")
)
.subscribe(
  obs -> subscribePrint(obs, obs.getKey()+" occurences of 'm'")
);

专辑标题按其中字母m的出现次数进行分组。文本被转换成所有字母出现的地方都被替换为*。输出如下:

0 occurences of 'm' : The Piper at the Gates of Dawn
0 occurences of 'm' : A Saucerful of Secrets
1 occurences of 'm' : *ore
4 occurences of 'm' : U**agu**a
2 occurences of 'm' : Ato* Heart *other
1 occurences of 'm' : *eddle
0 occurences of 'm' : Obscured by Clouds
1 occurences of 'm' : The Dark Side of the *oon
0 occurences of 'm' : Wish You Were Here
1 occurences of 'm' : Ani*als
0 occurences of 'm' : The Wall

注意

使用Observable.groupBy()操作符的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter04/UsingGroupBy.java找到。

其他有用的转换操作符

还有一些其他值得一提的转换。例如,有cast()操作符,它是map(v -> someClass.cast(v))的快捷方式。

List<Number> list = Arrays.asList(1, 2, 3);
Observable<Integer> iObs = Observable
  .from(list)
  .cast(Integer.class);

这里的初始Observable实例发出Number类型的值,但它们实际上是Integer实例,所以我们可以使用cast()操作符将它们表示为Integer实例。

另一个有用的操作符是timestamp()操作符。它通过将每个发出的值转换为Timestamped<T>类的实例来为其添加时间戳。例如,如果我们想要记录Observable的输出,这将非常有用。

List<Number> list = Arrays.asList(3, 2);
Observable<Timestamped<Number>> timestamp = Observable
  .from(list)
  .timestamp();
subscribePrint(timestamp, "Timestamps");

在这个例子中,每个数字都被时间戳标记。同样,可以使用map()操作符很容易地实现。前面例子的输出如下:

Timestamps : Timestamped(timestampMillis = 1431184924388, value = 1)
Timestamps : Timestamped(timestampMillis = 1431184924394, value = 2)
Timestamps : Timestamped(timestampMillis = 1431184924394, value = 3)

另一个类似的操作符是timeInterval操作符,但它将一个值转换为TimeInterval<T>实例。TimeInterval<T>实例表示Observable发出的项目以及自上一个项目发出以来经过的时间量,或者(如果没有上一个项目)自订阅以来经过的时间量。这可以用于生成统计信息,例如:

Observable<TimeInterval<Long>> timeInterval = Observable
  .timer(0L, 150L, TimeUnit.MILLISECONDS)
  .timeInterval();
subscribePrint(timeInterval, "Time intervals");

这将输出类似于这样的内容:

Time intervals : TimeInterval [intervalInMilliseconds=13, value=0]
Time intervals : TimeInterval [intervalInMilliseconds=142, value=1]
Time intervals : TimeInterval [intervalInMilliseconds=149, value=2]
...................................................................

我们可以看到不同的值大约在 150 毫秒左右发出,这是应该的。

timeIntervaltimestamp操作符都在immediate调度程序上工作(参见第六章,“使用调度程序进行并发和并行处理”),它们都以毫秒为单位保留其时间信息。

注意

前面示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter04/VariousTransformationsDemonstration.java找到。

过滤数据

在第一章的响应式求和示例中,我们根据特殊模式过滤用户输入。例如,模式是* a:。通常只从数据流中过滤出有趣的数据。例如,仅从所有按键按下事件中过滤出*按键按下事件,或者仅从文件中包含给定表达式的行中过滤出行。这就是为什么不仅能够转换我们的数据,还能够学会如何过滤它是很重要的。

RxJava 中有许多过滤操作符。其中最重要的是filter()。它的弹珠图非常简单,如下所示:

过滤数据

它显示filter()操作符通过某些属性过滤数据。在图中,它是元素的形式:它只过滤圆圈。像所有其他操作符一样,filter()从源创建一个新的Observable实例。这个Observable实例只发出符合filter()操作符定义的条件的项目。以下代码片段说明了这一点:

Observable<Integer> numbers = Observable
  .just(1, 13, 32, 45, 21, 8, 98, 103, 55);
Observable<Integer> filter = numbers
  .filter(n -> n % 2 == 0);
subscribePrint(filter, "Filter");

这将仅输出偶数32898),因为满足过滤条件。

filter()操作符根据用户定义的函数过滤元素。还有一些其他过滤操作符。为了理解它们,让我们看一些简单的例子:

Observable<Integer> numbers = Observable
  .just(1, 13, 32, 45, 21, 8, 98, 103, 55);
Observable<String> words = Observable
  .just(
    "One", "of", "the", "few", "of",
    "the", "crew", "crew"
  );
Observable<?> various = Observable
  .from(Arrays.asList("1", 2, 3.0, 4, 5L));

我们定义了三个Observable实例来用于我们的示例。第一个发出九个数字。第二个逐个发出句子中的所有单词。第三个发出不同类型的元素——字符串、整数、双精度和长整型。

subscribePrint(numbers.takeLast(4), "Last 4");

takeLast()操作符返回一个新的Observable实例,只从源Observable实例中发出最后的N个项目,只有当它完成时。这个方法有一些重载。例如,有一个可以在指定的时间窗口内发出源的最后N个或更少的项目。另一个可以接收一个Scheduler实例,以便在另一个线程上执行。

在这个例子中,只有Observable实例的最后四个项目将被过滤和输出:

Last 4 : 8
Last 4 : 98
Last 4 : 103
Last 4 : 55
Last 4 ended!

让我们来看下面的代码片段:

subscribePrint(numbers.last(), "Last");

last()操作符创建的Observable实例,在源Observable实例完成时只输出最后一个项目。如果源没有发出项目,将会发出NoSuchElementException异常作为OnError() 通知。它有一个重载,接收一个类型为T->Boolean的谓词参数。因此,它只发出源发出的最后一个符合谓词定义的条件的项目。在这个例子中,输出将如下所示:

Last : 55
Last ended!

takeLastBuffer()方法的行为与takeLast()方法类似,但它创建的Observable实例只发出一个包含源的最后N个项目的List实例:

subscribePrint(
  numbers.takeLastBuffer(4), "Last buffer"
);

它有类似的重载。这里的输出如下:

Last buffer : [8, 98, 103, 55]
Last buffer ended!

lastOrDefault()操作符的行为与last()操作符相似,并且具有谓词的相同重载:

subscribePrint(
  numbers.lastOrDefault(200), "Last or default"
);
subscribePrint(
  Observable.empty().lastOrDefault(200), "Last or default"
);

然而,如果源没有发出任何东西,lastOrDefault()操作符会发出默认值而不是OnError 通知。这个例子的输出如下:

Last or default : 55
Last or default ended!
Last or default : 200
Last or default ended!

skipLast()操作符是takeLast()方法的完全相反;它在完成时发出除了源的最后N个项目之外的所有内容:

subscribePrint(numbers.skipLast(4), "Skip last 4");

它有类似的重载。这个例子的输出如下:

Skip last 4 : 1
Skip last 4 : 13

skip()方法与skipLast()方法相同,但是跳过前N个项目而不是最后一个:

subscribePrint(numbers.skip(4), "Skip 4");

这意味着示例的输出如下:

Skip 4 : 21
Skip 4 : 8
Skip 4 : 98
Skip 4 : 103
Skip 4 : 55
Skip 4 ended!

take()操作符类似于takeLast()操作符,但是它发出源的前N个项目,而不是最后的N个项目。

subscribePrint(numbers.take(4), "First 4");

这是一个常用的操作符,比takeLast()操作符更便宜,因为takeLast()操作符会缓冲其项目并等待源完成。这个操作符不会缓冲其项目,而是在接收到它们时发出它们。它非常适用于限制无限的Observable实例。前面例子的输出如下:

First 4 : 1
First 4 : 13
First 4 : 32
First 4 : 45
First 4 ended!

让我们来看下面的代码片段:

subscribePrint(numbers.first(), "First");

first()操作符类似于last()操作符,但只发出源发出的第一个项目。如果没有第一个项目,它会发出相同的OnError 通知。它的谓词形式有一个别名——takeFirst()操作符。还有一个firstOrDefault()操作符形式。这个例子的输出很清楚:

First : 1
First ended!

让我们来看下面的代码片段:

subscribePrint(numbers.elementAt(5), "At 5");

elementAt()操作符类似于first()last()操作符,但没有谓词形式。不过有一个elementAtOrDefault()形式。它只发出源Observable实例发出的项目序列中指定索引处的元素。这个例子输出如下:

At 5 : 8
At 5 ended!

让我们来看下面的代码片段:

subscribePrint(words.distinct(), "Distinct");

distinct()操作符产生的Observable实例发出源的项目,排除重复的项目。有一个重载可以接收一个函数,返回一个用于决定一个项目是否与另一个项目不同的键或哈希码值:

Distinct : One
Distinct : of
Distinct : the
Distinct : few
Distinct : crew
Distinct ended!

subscribePrint(
  words.distinctUntilChanged(), "Distinct until changed"
);

distinctUntilChanged()操作符类似于distinct()方法,但它返回的Observable实例会发出源Observable实例发出的所有与它们的直接前导不同的项目。因此,在这个例子中,它将发出除了最后一个crew之外的每个单词。

subscribePrint( // (13)
  various.ofType(Integer.class), "Only integers"
);

ofType()操作符创建一个只发出给定类型源发出的项目的Observable实例。它基本上是这个调用的快捷方式:filter(v -> Class.isInstance(v))。在这个例子中,输出将如下所示:

Only integers : 2
Only integers : 4
Only integers ended!

注意

所有这些示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter04/FilteringExamples.java上查看/下载。

这些是 RxJava 提供的最常用的过滤操作符。我们将在以后的示例中经常使用其中的一些。

在本章中,我们将要看的last操作符是一个转换操作符,但有点特殊。它可以使用先前累积的状态!让我们了解更多。

累积数据

scan(Func2)操作符接受一个带有两个参数的函数作为参数。它的结果是一个Observable实例。通过scan()方法的结果发出的第一个项目是源Observable实例的第一个项目。发出的第二个项目是通过将传递给scan()方法的函数应用于结果Observable实例之前发出的项目和源Observable实例发出的第二个项目来创建的。通过scan()方法结果发出的第三个项目是通过将传递给scan()方法的函数应用于之前发出的项目和源Observable实例发出的第三个项目来创建的。这种模式继续下去,以创建scan()方法创建的Observable实例发出的序列的其余部分。传递给scan()方法的函数称为累加器

让我们来看一下scan(Func2)方法的弹珠图:

累积数据

scan()方法发出的项目可以使用累积状态生成。在图中,圆圈在三角形中累积,然后这个三角形圆圈在正方形中累积。

这意味着我们可以发出一系列整数的总和,例如:

Observable<Integer> scan = Observable
  .range(1, 10)
  .scan((p, v) -> p + v);
subscribePrint(scan, "Sum");
subscribePrint(scan.last(), "Final sum");

第一个订阅将输出所有的发射:1, 3 (1+2), 6 (3 + 3), 10 (6 + 4) .. 55。但在大多数情况下,我们只对最后发出的项目感兴趣——最终总和。我们可以使用一个只发出最后一个元素的Observable实例,使用last()过滤操作符。值得一提的是,还有一个reduce(Func2)操作符,是scan(Func2).last()的别名。

scan()操作符有一个重载,可以与seed/initial参数一起使用。在这种情况下,传递给scan(T, Func2)操作符的函数被应用于源发出的第一个项目和这个seed参数。

Observable<String> file = CreateObservable.from(
  Paths.get("src", "main", "resources", "letters.txt")
);
scan = file.scan(0, (p, v) -> p + 1);
subscribePrint(scan.last(), "wc -l");

这个示例计算文件中的行数。文件Observable实例逐行发出指定路径文件的行。我们使用scan(T, Func2)操作符,初始值为0,通过在每行上累加计数来计算行数。

我们将用一个示例来结束本章,其中使用了本章介绍的许多操作符。让我们来看一下:

Observable<String> file = CreateObservable.from(
  Paths.get("src", "main", "resources", "operators.txt")
);
Observable<String> multy = file
  .flatMap(line -> Observable.from(line.split("\\."))) // (1)
  .map(String::trim) // (2)
  .map(sentence -> sentence.split(" ")) // (3)
  .filter(array -> array.length > 0) // (4)
  .map(array -> array[0]) // (5)
  .distinct() // (6)
  .groupBy(word -> word.contains("'")) //(7)
  .flatMap(observable -> observable.getKey() ? observable : // (8)
    observable.map(Introspector::decapitalize))
  .map(String::trim) // (9)
  .filter(word -> !word.isEmpty()) // (10)
  .scan((current, word) -> current + " " + word) // (11)
  .last() // (12)
  .map(sentence -> sentence + "."); // (13)
subscribePrint(multy, "Multiple operators"); // (14)

这段代码使用了许多操作符来过滤并组装隐藏在文件中的句子。文件由一个Observable实例表示,它逐行发出其中包含的所有行。

  1. 我们不只想对不同的行进行操作;我们想发出文件中包含的所有句子。因此,我们使用flatMap操作符创建一个逐句发出文件句子的Observable实例(由dot确定)。

  2. 我们使用map()操作符修剪这些句子。它可能包含一些前导或尾随空格。

  3. 我们希望对句子中包含的不同单词进行操作,因此我们使用map()操作符和String::split参数将它们转换为单词数组。

  4. 我们不关心空句子(如果有的话),所以我们使用filter()操作符将它们过滤掉。

  5. 我们只需要句子中的第一个单词,所以我们使用map()操作符来获取它们。生成的Observable实例会发出文件中每个句子的第一个单词。

  6. 我们不需要重复的单词,所以我们使用distinct()操作符来摆脱它们。

  7. 现在我们想以某种方式分支我们的逻辑,使一些单词被不同对待。所以我们使用groupBy()操作符和一个Boolean键将我们的单词分成两个Observable实例。选择的单词的键是True,其他的是False

  8. 使用flatMap操作符,我们连接我们分开的单词,但只有选择的单词(带有True键)保持不变。其余的被小写

  9. 我们使用map()操作符去除所有不同单词的前导/尾随空格。

  10. 我们使用filter()操作符来过滤掉空的句子。

  11. 使用scan()操作符,我们用空格作为分隔符连接单词。

  12. 使用last()操作符,我们的结果Observable实例将只发出最后的连接,包含所有单词。

  13. 最后一次调用map()操作符,通过添加句点从我们连接的单词中创建一个句子。

  14. 如果我们输出这个Observable实例发出的单个项目,我们将得到一个由初始文件中所有句子的第一个单词组成的句子(跳过重复的单词)!

输出如下:

Multiple operators : I'm the one who will become RX.
Multiple operators ended!

注意

上述示例可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter04/VariousTransformationsDemonstration.java找到。

总结

本章结尾的示例演示了我们迄今为止学到的内容。我们可以通过链接Observable实例并使用各种操作符来编写复杂的逻辑。我们可以使用map()flatMap()操作符来转换传入的数据,并可以使用groupBy()filter()操作符或不同的flatMap()操作符来分支逻辑。我们可以再次使用flatMap()操作符将这些分支连接起来。我们可以借助不同的过滤器选择数据的部分,并使用scan()操作符累积数据。使用所有这些操作符,我们可以以可读且简单的方式编写相当不错的程序。程序的复杂性不会影响代码的复杂性。

下一步是学习如何以更直接的方式组合我们逻辑的分支。我们还将学习如何组合来自不同来源的数据。所以让我们继续下一章吧!

第五章:组合器,条件和错误处理

我们编写的大多数程序都处理来自不同来源的数据。这些来源既可以是外部的(文件、数据库、服务器等)也可以是内部的(不同的集合或相同外部来源的分支)。有许多情况下,我们希望这些来源以某种方式相互依赖。定义这些依赖关系是构建我们的程序的必要步骤。本章的目的是介绍能够实现这一点的Observable操作符。

我们在第一章和第二章中看到了组合的Observable实例的例子。我们的“响应式求和”程序有一个外部数据源——用户输入,但它根据自定义格式分成了两个内部数据源。我们看到了如何使用filter()操作符而不是过程式的if-else构造。后来,我们借助组合器将这些数据流合并成一个。

我们将学习如何在Observable实例链中对错误做出反应。记住,能够对失败做出反应使我们的程序具有弹性。

在本章中,我们将涵盖:

  • 使用操作符(如combineLatest()merge()concat()zip())组合Observable实例

  • 使用条件操作符(如takeUntil()skipUntil()amb())在Observable实例之间创建依赖关系

  • 使用retry()onErrorResumeNext()onErrorReturn()等操作符进行错误处理

组合 Observable 实例

我们首先来看一下zip(Observable, Observable, <Observable>..., Func)操作符,它可以使用组合函数组合两个或多个Observable实例。

zip 操作符

传递给zip操作符的函数的参数数量与传递给zip()方法的Observable实例的数量一样多。当所有这些Observable实例至少发出一项时,将使用每个Observable实例首次发出的参数值调用该函数。其结果将是通过zip()方法创建的Observable实例的第一项。由这个Observable实例发出的第二项将是源Observable实例的第二项的组合(使用zip()方法的函数参数计算)。即使其中一个源Observable实例已经发出了三项或更多项,它的第二项也会被使用。结果的Observable实例总是发出与源Observable实例相同数量的项,它发出最少的项然后完成。

这种行为在下面的弹珠图中可以更清楚地看到:

zip 操作符

这是一个非常简单的使用zip()方法的例子:

Observable<Integer> zip = Observable
.zip(
 Observable.just(1, 3, 4),
 Observable.just(5, 2, 6),
 (a, b) -> a + b
);
subscribePrint(zip, "Simple zip");

这个例子类似于弹珠图,并输出相同的结果。由zip()方法创建的Observable实例发出的第一项是在所有源至少发出一项之后发出的。这意味着即使其中一个源发出了所有的项,结果也只会在所有其他源发出项时才会被发出。

现在,如果你还记得来自第三章的interval()操作符,它能够创建一个Observable实例,每<n>毫秒发出一个顺序数字。如果你想要发出一系列任意对象,可以通过使用zip()方法结合interval()from()just()方法来实现。让我们看一个例子:

Observable<String> timedZip = Observable
.zip(
 Observable.from(Arrays.asList("Z", "I", "P", "P")),
 Observable.interval(300L, TimeUnit.MILLISECONDS),
 (value, i) -> value
);
subscribePrint(timedZip, "Timed zip");

这将在 300 毫秒后输出Z,在另外 300 毫秒后输出I,在相同的间隔后输出P,并在另外 300 毫秒后输出另一个P。之后,timedZip Observable实例将完成。这是因为通过interval()方法创建的源Observable实例每 300 毫秒发出一个元素,并确定了timedZip参数发射的速度。

zip()方法也有一个实例方法版本。该操作符称为zipWith()。以下是一个类似的示例,但使用了zipWith()操作符:

Observable<String> timedZip = Observable
.from(Arrays.asList("Z", "I", "P", "P"))
.zipWith(
 Observable.interval(300L, TimeUnit.MILLISECONDS),
 (value, skip) -> value
);
subscribePrint(timedZip, "Timed zip");

接下来,我们将了解在实现“反应式求和”时在第一章中首次看到的组合器反应式编程简介

combineLatest 操作符

combineLatest()操作符具有与zip()操作符相同的参数和重载,但行为有些不同。它创建的Observable实例在每个源至少有一个时立即发出第一个项目,取每个源的最后一个。之后,它创建的Observable实例在任何源Observable实例发出项目时发出项目。combineLatest()操作符发出的项目数量完全取决于发出的项目顺序,因为在每个源至少有一个之前,单个源可能会发出多个项目。它的弹珠图看起来像这样:

The combineLatest operator

在上图中,由组合的Observable实例发出的项目的颜色与触发它们发出的项目的颜色相同。

在接下来的几个示例中,将使用由interval()zipWith()方法创建的三个源Observable实例:

Observable<String> greetings = Observable
.just("Hello", "Hi", "Howdy", "Zdravei", "Yo", "Good to see ya")
.zipWith(
  Observable.interval(1L, TimeUnit.SECONDS),
  this::onlyFirstArg
);
Observable<String> names = Observable
.just("Meddle", "Tanya", "Dali", "Joshua")
.zipWith(
  Observable.interval(1500L, TimeUnit.MILLISECONDS),
  this::onlyFirstArg
);
Observable<String> punctuation = Observable
.just(".", "?", "!", "!!!", "...")
.zipWith(
  Observable.interval(1100L, TimeUnit.MILLISECONDS),
  this::onlyFirstArg
);

这是用于压缩的函数:

public <T, R> T onlyFirstArg(T arg1, R arg2) {
  return arg1;
}

这是在关于zip()方法的部分中看到的在发射之间插入延迟的相同方法。这三个Observable实例可以用来比较不同的组合方法。包含问候的Observable实例每秒发出一次,包含名称的实例每 1.5 秒发出一次,包含标点符号的实例每 1.1 秒发出一次。

使用combineLatest()操作符,我们可以这样组合它们:

Observable<String> combined = Observable
.combineLatest(
 greetings, names, punctuation,
 (greeting, name, puntuation) ->
 greeting + " " + name + puntuation)
;
subscribePrint(combined, "Sentences");

这将组合不同源的项目成句。第一句将在一秒半后发出,因为所有源都必须发出某些内容,以便组合的Observable实例开始发出。这句话将是'Hello Meddle.'。下一句将在任何源发出内容时立即发出。这将在订阅后两秒后发生,因为问候Observable实例每秒发出一次;它将发出'Hi',这将使组合的Observable实例发出'Hi Meddle.'。当经过 2.2 秒时,标点Observable实例将发出'?',所以我们将有另一句话——'Hi Meddle?'。这将持续到所有源完成为止。

当我们需要计算或通知依赖的任何数据源发生更改时,combineLatest()操作符非常有用。下一个方法更简单;它只是合并其源的发射,交错它们的过程。

合并操作符

当我们想要从多个源获取数据作为一个流时,我们可以使用merge()操作符。例如,我们可以有许多Observable实例从不同的log文件中发出数据。我们不关心当前发射的数据来自哪个log文件,我们只想看到所有的日志。

merge()操作符的图表非常简单:

The merge operator

每个项目都在其原始发射时间发出,源无关紧要。使用前一节介绍的三个Observable实例的示例如下:

Observable<String> merged = Observable
  .merge(greetings, names, punctuation);
subscribePrint(merged, "Words");

它只会发出不同的单词/标点符号。第一个发出的单词将来自问候Observable实例,在订阅后一秒钟发出(因为问候每秒发出一次)'Hello';然后在 100 毫秒后发出'.',因为标点Observable实例每 1.1 秒发出一次。在订阅后 400 毫秒,也就是一秒半后,将发出'Meddle'。接下来是问候'Hi'。发射将继续进行,直到最耗时的源Observable实例完成。

值得一提的是,如果任何源发出OnError通知,merge Observable实例也会发出error并随之完成。有一种merge()操作符的形式,延迟发出错误,直到所有无错误的源Observable实例都完成。它被称为mergeDelayError()

如果我们想以这样的方式组合我们的源,使它们的项目不会在时间上交错,并且第一个传递的源的发射优先于下一个源,我们将使用本章介绍的最后一个组合器——concat()操作符。

连接运算符

这本书的所有章节都在不同的文件中。我们想要将所有这些文件的内容连接成一个大文件,代表整本书。我们可以为每个章节文件创建一个Observable实例,使用我们之前创建的from(Path)方法,然后我们可以使用这些Observable实例作为源,使用concat()操作符将它们按正确的顺序连接成一个Observable实例。如果我们订阅这个Observable实例,并使用一个将所有内容写入文件的方法,最终我们将得到我们的书文件。

请注意,conact()操作符不适用于无限的Observable实例。它将发出第一个的通知,但会阻塞其他的。merge()concat()操作符之间的主要区别在于,merge()同时订阅所有源Observable实例,而concat()在任何时候只有一个订阅。

concat()操作符的弹珠图如下:

连接运算符

以下是连接前面示例中的三个Observable实例的示例:

Observable<String> concat = Observable
  .concat(greetings, names, punctuation);
subscribePrint(concat, "Concat");

这将每秒一个地输出所有的问候,然后每秒半输出名字,最后每 1.1 秒输出标点符号。在问候和名字之间将有 1.5 秒的间隔。

有一个操作符,类似于concat()操作符,称为startWith()。它将项目前置到Observable实例,并具有重载,可以接受一个、两个、三个等等,最多九个值,以及Iterable实例或另一个Observable实例。使用接受另一个Observable实例作为参数的重载,我们可以模拟concat()操作符。以下是前面示例在以下代码中的实现:

Observable<String> concat = punctuation
  .startWith(names)
 .startWith(greetings);
subscribePrint(concat, "Concatenated");

问候Observable实例被前置到名字之前,这个结果被前置到标点的Observable实例,创建了与前面示例中相同的连接源的Observable实例。

注意

本章中前面和所有之前示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter05/CombiningObservables.java找到。

startWith()操作符的良好使用是与combineLatest()操作符一起使用。如果你记得我们'Reactive Sum'示例的初始实现,你必须输入ab的值才能计算初始和。但是假设我们修改和的构造方式如下:

Observable.combineLatest(
  a.startWith(0.0),
  b.startWith(0.0),
  (x, y) -> x + y
);

即使用户还没有输入任何内容,我们将有一个初始总和为0.0的情况,以及用户第一次输入a但尚未给b赋值的情况,这种情况下我们不会看到总和发生。

merge()操作符一样,concat()操作符也有一个实例形式——concatWith()操作符。

在本章的这一部分,我们看到了如何组合不同的Observable实例。但是组合并不是Observable实例之间唯一的交互。它们可以相互依赖或管理彼此。有一种方法可以让一个或多个Observable实例创建条件,改变其他Observable实例的行为。这是通过条件操作符来实现的。

条件操作符

可以使一个Observable实例在另一个发出之前不开始发出,或者只在另一个不发出任何内容时才发出。这些Observable实例能够在给定条件下发出项目,并且这些条件是使用条件操作符应用到它们上的。在本节中,我们将看一些 RxJava 提供的条件操作符。

amb 操作符

amb()操作符有多个重载,可以接受从两个到九个源Observable实例,或者是一个包含Observable实例的Iterable实例。它会发出首先开始发出的源Observable实例的项目。无论是OnErrorOnCompleted通知还是数据,都不重要。它的图表看起来像这样:

amb 操作符

这个操作符也有一个实例形式。它被称为ambWith(),可以在一个Observable实例上调用,作为参数传入另一个Observable实例。

这个条件操作符适用于从多个类似数据源中读取数据。订阅者不需要关心数据的来源。它可以用于实现简单的缓存,例如。这里有一个小例子,展示了它的使用方法:

Observable<String> words = Observable.just("Some", "Other");
Observable<Long> interval = Observable
  .interval(500L, TimeUnit.MILLISECONDS)
  .take(2);
subscribePrint(Observable.amb(words, interval), "Amb 1");
Random r = new Random();
Observable<String> source1 = Observable
  .just("data from source 1")
  .delay(r.nextInt(1000), TimeUnit.MILLISECONDS);
Observable<String> source2 = Observable
  .just("data from source 2")
  .delay(r.nextInt(1000), TimeUnit.MILLISECONDS);
subscribePrint(Observable.amb(source1, source2), "Amb 2");

第一个amb()操作符将发出words Observable实例的项目,因为interval Observable实例需要等待半秒钟才能发出,而words会立即开始发出。

第二个amb Observable实例的发射将是随机决定的。如果第一个源Observable实例在第二个之前发出数据,那么amb Observable实例将会发出相同的数据,但如果第二个源先发出,那么amb Observable实例将发出它的数据。

takeUntil()、takeWhile()、skipUntil()和 skipWhile()条件操作符

我们在上一章中看到了类似的操作符。take(int)操作符仅过滤了前n个项目。这些操作符也过滤项目,但是基于条件takeUntil()操作符接受另一个Observable实例,直到这个其他Observable实例发出,源的项目才会被发出;之后,由takeUntil()操作符创建的Observable实例将完成。让我们看一个使用这些操作符的例子:

Observable<String> words = Observable // (1)
  .just("one", "way", "or", "another", "I'll", "learn", "RxJava")
  .zipWith(
    Observable.interval(200L, TimeUnit.MILLISECONDS),
    (x, y) -> x
  );
Observable<Long> interval = Observable
  .interval(500L, TimeUnit.MILLISECONDS);
subscribePrint(words.takeUntil(interval), "takeUntil"); // (2)
subscribePrint( // (3)
  words.takeWhile(word -> word.length() > 2), "takeWhile"
);
subscribePrint(words.skipUntil(interval), "skipUntil"); // (4)

让我们看一下以下解释:

  1. 在这些例子中,我们将使用wordsinterval Observable实例。words Observable实例每 200 毫秒发出一个单词,而interval Observable每半秒发出一次。

  2. 如前所述,takeUntil()操作符的这种重载将在interval Observable发出之前发出单词。因此,oneway将被发出,因为下一个单词or应该在订阅后的 600 毫秒后发出,而interval Observable在第 500 毫秒时发出。

  3. 在这里,takeWhile()运算符对words Observable设置了条件。它只会在有包含两个以上字母的单词时发出。因为'or'有两个字母,所以它不会被发出,之后的所有单词也会被跳过。takeUntil()运算符有一个类似的重载,但它只会发出包含少于三个字母的单词。没有takeWhile(Observable)运算符重载,因为它本质上是zip()运算符:只有在另一个发出时才发出。

  4. skip*运算符类似于take*运算符。不同之处在于它们在满足条件之前/之后不会发出。在这个例子中,单词oneway被跳过,因为它们在订阅的 500 毫秒之前被发出,而interval Observable在 500 毫秒时开始发出。单词'or'和之后的所有单词都被发出。

这些条件运算符可以用于在 GUI 应用程序中显示加载动画。代码可能是这样的:

loadingAnimationObservable.takeUntil(requestObservable);

在每次发出loadingAnimationObservable变量时,都会向用户显示一些短暂的动画。当请求返回时,动画将不再显示。这是程序逻辑的另一种分支方式。

defaultIfEmpty()运算符

defaultIfEmpty()运算符的想法是,如果未知的源为空,就返回一些有用的东西。例如,如果远程源没有新内容,我们将使用本地存储的信息。

这是一个简单的例子:

Observable<Object> test = Observable
  .empty()
  .defaultIfEmpty(5);
subscribePrint(test, "defaultIfEmpty");

当然,这将输出5并完成。

注意

amb()take*skip*defaultIfEmpty()运算符示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter05/Conditionals.java找到。

到目前为止,我们已经转换、过滤和组合了数据。但是错误呢?我们的应用程序随时可能进入错误状态。是的,我们可以订阅Observable实例发出的错误,但这将终止我们的逻辑。在subscribe方法中,我们已经超出了操作链。如果我们想要在Observable实例链内部对错误做出反应,并尝试阻止终止怎么办?有一些运算符可以帮助我们做到这一点,我们将在下一节中对它们进行检查。

处理错误

在处理 RxJava 中的错误时,您应该意识到它们会终止Observable的操作链。就像处理常规的过程代码一样,一旦进入 catch 块,就无法返回到抛出异常的代码。但是您可以执行一些备用逻辑,并在程序失败时使用它。return*retry*resume*运算符做了类似的事情。

返回和恢复运算符

onErrorReturn运算符可用于防止调用Subscriber实例的onError。相反,它将发出最后一个项目并完成。这是一个例子:

Observable<String> numbers = Observable
  .just("1", "2", "three", "4", "5")
  .map(Integer::parseInt)
  .onErrorReturn(e -> -1);
  subscribePrint(numbers, "Error returned");

Integer::parseInt方法将成功地将字符串12转换为Integer值,但在three上会失败,并引发NumberFormatException异常。此异常将传递给onErrorReturn()方法,它将返回数字-1numbers Observable实例将发出数字-1并完成。因此输出将是12-1OnCompleted通知。

这很好,但有时我们会希望在发生异常时切换到另一个 Observable 操作链。为此,我们可以使用onExceptionResumeNext()运算符,它在发生Exception时返回一个备用的Observable实例,用于替换源实例。以下是修改后使用它的代码:

Observable<Integer> defaultOnError =
  Observable.just(5, 4, 3, 2, 1);
Observable<String> numbers = Observable
  .just("1", "2", "three", "4", "5")
  .map(Integer::parseInt)
  .onExceptionResumeNext(defaultOnError);
  subscribePrint(numbers, "Exception resumed");

现在这将输出1254321OnCompleted通知,因为在'three'引发异常后,传递给onExceptionResumeNext()方法的defaultOnError Observable实例将开始发出,替换所有Subscriber方法的源Observable实例。

还有一个非常类似于onExceptionResumeNext()resuming()操作符。它被称为onErrorResumeNext()。它可以替换前面示例中的onExceptionResumeNext()操作符,结果将是相同的。不过这两个操作符之间有两个区别。

首先,onErrorResumeNext()操作符有一个额外的重载,它接受一个 lambda 表达式,返回Observable实例(类似于onErrorReturn()方法)。其次,它将对每种错误做出反应。onExceptionResumeNext()方法只对Exception类及其子类的实例做出反应。

Observable<String> numbers = Observable
  .just("1", "2", "three", "4", "5")
  .doOnNext(number -> {
    assert !number.equals("three");
  }
  .map(Integer::parseInt)
  .onErrorResumeNext(defaultOnError);
  subscribePrint(numbers, "Error resumed");

在这个示例中,结果将与前一个示例相同(1, 2, 5, 4, 3, 2, 1, OnCompleted notification b)断言错误并不重要。但是如果我们使用了onExceptionResumeNext()操作符,错误将作为OnError notification到达subscribePrint方法。

在这个示例中使用的doOnNext()操作符是一个副作用生成器。它不会改变被调用的Observable实例发出的项目。它可以用于日志记录、缓存、断言或添加额外的逻辑。还有doOnError()doOnCompleted()操作符。此外,还有一个finallyDo()操作符,当出现错误或Observable实例完成时,它会执行传递给它的函数。

重试技术

重试是一种重要的技术。当一个Observable实例从不确定的来源(例如远程服务器)发出数据时,一个网络问题可能会终止整个应用程序。在错误上重试可以在这种情况下拯救我们。

retry()操作符插入Observable操作链中意味着如果发生错误,订阅者将重新订阅源Observable实例,并从链的开头尝试一切。如果再次出现错误,一切将再次重新开始。没有参数的retry()操作符会无限重试。还有一个重载的retry(int)方法,它接受最大允许的重试尝试次数。

为了演示retry()方法,我们将使用以下特殊行为:

class FooException extends RuntimeException {
  public FooException() {
    super("Foo!");
  }
}

class BooException extends RuntimeException {
  public BooException() {
    super("Boo!");
  }
}
class ErrorEmitter implements OnSubscribe<Integer> {
  private int throwAnErrorCounter = 5;
  @Override
  public void call(Subscriber<? super Integer> subscriber) {
    subscriber.onNext(1);
    subscriber.onNext(2);
    if (throwAnErrorCounter > 4) {
      throwAnErrorCounter--;
      subscriber.onError(new FooException());
      return;
    }
    if (throwAnErrorCounter > 0) {
      throwAnErrorCounter--;
      subscriber.onError(new BooException());
      return;
    }
    subscriber.onNext(3);
    subscriber.onNext(4);
    subscriber.onCompleted();
    }
  }
}

可以将一个ErrorEmitter实例传递给Observable.create()方法。如果throwAnErrorCounter字段的值大于四,就会发送一个FooException异常;如果大于零,就会发送一个BooException异常;如果小于或等于零,就会发送一些事件并正常完成。

现在让我们来看一下使用retry()操作符的示例:

subscribePrint(Observable.create(new ErrorEmitter()).retry(), "Retry");

因为throwAnErrorCounter字段的初始值是五,它将重试次,当计数器变为零时,Observable实例将完成。结果将是12121212121234OnCompleted通知。

retry()操作符可用于重试一组次数(或无限次)。它甚至有一个重载,接受一个带有两个参数的函数——目前的重试次数和Throwable实例的原因。如果这个函数返回TrueObservable实例将重新订阅。这是一种编写自定义重试逻辑的方法。但是延迟重试呢?例如,每秒重试一次?有一个特殊的操作符能够处理非常复杂的重试逻辑,那就是retryWhen()操作符。让我们来看一个使用它以及之前提到的retry(predicate)操作符的示例:

Observable<Integer> when = Observable.create(new ErrorEmitter())
  .retryWhen(attempts -> {
 return attempts.flatMap(error -> {
 if (error instanceof FooException) {
 System.err.println("Delaying...");
 return Observable.timer(1L, TimeUnit.SECONDS);
 }
 return Observable.error(error);
 });
 })
  .retry((attempts, error) -> {
 return (error instanceof BooException) && attempts < 3;
 });
subscribePrint(when, "retryWhen");

retryWhen()操作符返回一个发出OnError()OnCompleted()通知的Observable实例时,通知被传播,如果没有其他retry/resume,则调用订阅者的onError()onCompleted()方法。否则,订阅者将重新订阅源 observable。

在这个例子中,如果ExceptionFooExceptionretryWhen()操作符返回一个在一秒后发出的Observable实例。这就是我们如何实现带有延迟的重试。如果Exception不是FooException,它将传播到下一个retry(predicate)操作符。它可以检查error的类型和尝试次数,并决定是否应该传播错误或重试源。

在这个例子中,我们将获得一个延迟的重试,从retry(predicate)方法获得三次重试,第五次尝试时,订阅者将收到一个OnError通知,带有一个BooException异常。

注意

retry/resume/return示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter05/HandlingErrors.java找到。

本章的最后一节留给了一个更复杂的例子。我们将利用我们迄今为止的知识创建一个对远程 HTTP API 的请求,并处理结果,将其输出给用户。

一个 HTTP 客户端示例

让我们使用 RxJava 通过username检索有关 GitHub 用户存储库的信息。我们将使用先前用于将信息输出到系统输出的subscribePrint()函数。程序的想法是显示用户的所有公共存储库,这些存储库不是分叉。程序的主要部分如下所示:

String username = "meddle0x53";
Observable<Map> resp = githubUserInfoRequest(client, username);
subscribePrint(
  resp
  .map(json ->
    json.get("name") + "(" + json.get("language") + ")"),
  "Json"
);

这个程序使用了我的用户名(可以很容易地改为使用作为参数传递的username)来检索其公共存储库的信息。它打印出每个存储库的名称以及其中使用的主要编程语言。存储库由从传入的 JSON 文件生成的Map实例表示,因此我们可以从中读取存储库属性。

这些 JSON Map实例是由githubUserInfoRequest(client, username)方法创建的Observable实例发出的。client 参数是 Apache 的HttpAsyncClient类的一个实例。客户端能够执行异步 HTTP 请求,并且还有一个名为RxApacheHttp的额外的 RxJava 模块,它为我们提供了 RxJava 和 Apache HTTP 之间的绑定。我们将在我们的 HTTP 请求实现中使用它;你可以在github.com/ReactiveX/RxApacheHttp找到它。

提示

还有许多其他的 RxJava 项目,放在github.com/ReactiveX。其中一些非常有用。例如,我们在本书中实现的大多数from(Stream/Reader/File)方法在RxJavaString模块中有更好的实现。

下一步是实现githubUserInfoRequest(HttpAsyncClient, String)方法:

Observable<Map> githubUserInfoRequest(HttpAsyncClient client, String githubUser) {
  if (githubUser == null) { // (1)
    return Observable.<Map>error(
      new NullPointerException("Github user must not be null!")
    );
  }
  String url = "https://api.github.com/users/" + githubUser + "/repos";
  return requestJson(client, url) // (2)
  .filter(json -> json.containsKey("git_url")) // (3)
  .filter(json -> json.get("fork").equals(false));
}

这个方法也相当简单。

  1. 首先,我们需要有一个 GitHub 的username来执行我们的请求,所以我们对它进行一些检查。它不应该是null。如果是null,我们将返回一个发出errorObservable实例,发出带有NullPointerException异常的OnError通知。我们的打印订阅函数将把它显示给用户。

  2. 为了实际进行 HTTP 请求,我们将使用另一个具有签名requestJson(HttpAsyncClient, String)的方法。它返回发出 JSON 的Map实例的Observable实例。

  3. 如果用户不是真正的 GitHub 用户,或者我们已经超过了 GitHub API 的限制,GitHub 会向我们发送一个 JSON 消息。这就是为什么我们需要检查我们得到的 JSON 是否包含存储库数据或其他内容。表示存储库的 JSON 具有git_url键。我们使用这个键来过滤只表示 GitHub 存储库的 JSON。

  4. 我们只需要非分叉存储库;这就是为什么我们要对它们进行过滤。

这再次非常容易理解。到目前为止,我们的逻辑中只使用了map()filter()运算符,没有什么特别的。让我们看一下实际的 HTTP 请求实现:

Observable<Map> requestJson(HttpAsyncClient client, String url) {
  Observable<String> rawResponse = ObservableHttp
 .createGet(url, client)
 .toObservable() // (1)
  .flatMap(resp -> resp.getContent() // (2)
    .map(bytes -> new String(
      bytes,  java.nio.charset.StandardCharsets.UTF_8
    ))
  )
  .retry(5) // (3)
  .cast(String.class) // (4)
  .map(String::trim)
  .doOnNext(resp -> getCache(url).clear()); // (5)
  1. ObservableHttp类来自RxApacheHttp模块。它为我们执行异步 HTTP 请求,使用 Apache 的HttpClient实例。createGet(url, client)方法返回一个实例,可以使用toObservable()方法转换为实际的Observable实例。我们在这里就是这样做的。

  2. 当这个Observable实例接收到 HTTP 响应时,它将作为ObservableHttpResponse实例发出。这个实例有一个getContent()方法,它返回一个Observable<byte[]>对象,表示响应为字节序列。我们使用简单的map()运算符将这些字节数组转换为String对象。现在我们有一个由String对象表示的 JSON 响应。

  3. 如果连接到 GitHub 出现问题,我们将重试五次。

  4. 由于 Java 的类型系统,将其转换为String是必要的。此外,我们使用trim()方法从响应中删除任何尾随/前导空格。

  5. 我们清除了此 URL 的缓存信息。我们使用一个简单的内存中的 Map 实例从 URL 到 JSON 数据缓存实现,以便不重复多次发出相同的请求。我们如何填充这个缓存?我们很快就会在下面的代码中看到。让我们来看一下:

  // (6)
  Observable<String> objects = rawResponse
    .filter(data -> data.startsWith("{"))
    .map(data -> "[" + data + "]");
  Observable<String> arrays = rawResponse
    .filter(data -> data.startsWith("["));
  Observable<Map> response = arrays
 .ambWith(objects) // (7)
    .map(data -> { // (8)
      return new Gson().fromJson(data, List.class);
    })
    .flatMapIterable(list -> list) // (9)
    .cast(Map.class)
    .doOnNext(json -> getCache(url).add(json)); // (10)
  return Observable.amb(fromCache(url), response); // (11)
}
  1. 响应可以是 JSON 数组或 JSON 对象;我们在这里使用filter()运算符来分支我们的逻辑。将 JSON 对象转换为 JSON 数组,以便稍后使用通用逻辑。

  2. 使用ambWith()运算符,我们将使用从两个Observable实例中发出数据的那个,并将结果视为 JSON 数组。我们将有数组或对象 JSON,最终结果只是一个作为String对象发出 JSON 数组的Observable实例。

  3. 我们使用 Google 的 JSON 库将这个String对象转换为实际的 Map 实例列表。

  4. flatMapIterable()运算符将发出List实例的Observable实例扁平化为发出其内容的实例,即表示 JSON 的多个 Map 实例。

  5. 所有这些 Map 实例都被添加到内存中的缓存中。

  6. 使用amb()运算符,我们实现了回退到缓存的机制。如果缓存包含数据,它将首先发出,这些数据将被使用。

我们有一个使用Observable实例实现的 HTTP 数据检索的真实示例!这个请求的输出看起来像这样:

Json : of-presentation-14(JavaScript)
Json : portable-vim(null)
Json : pro.js(JavaScript)
Json : tmangr(Ruby)
Json : todomvc-proact(JavaScript)
Json : vimconfig(VimL)
Json : vimify(Ruby)
Json ended!

注意

上述示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter05/HttpRequestsExample.java找到。

摘要

在本章中,我们学习了如何组合Observable实例,如何在它们之间创建依赖关系,以及如何对错误做出反应。正如我们在最后的例子中看到的,我们现在能够使用只有Observable实例和它们的运算符来创建相当复杂的逻辑。再加上互联网上可用的 RxJava 模块,我们几乎可以将每个数据源转换为Observable实例。

下一步是掌握调度器。它们将为我们提供处理多线程的能力,同时在编码时使用这种响应式编程风格。Java 以其并发性而闻名;现在是时候将语言的这些能力添加到我们的Observable链中,以并行方式执行多个 HTTP 请求(例如)。我们将学习的另一件新事情是如何对我们的数据进行缓冲节流去抖动,这些技术与实时数据流息息相关。

第六章:使用调度程序进行并发和并行处理

现代处理器具有多个核心,并且能够同时更快地处理许多耗时操作。Java 并发 API(包括线程等)使这成为可能。

RxJava 的Observable链似乎很适合线程。如果我们可以在后台订阅我们的源并进行所有的转换、组合和过滤,然后在一切都完成时将结果传递给主线程,那将是很棒的。是的,这听起来很美好,但是 RxJava 默认是单线程的。这意味着,在大多数情况下,当在Observable实例上调用subscribe方法时,当前线程会阻塞直到所有内容被发出。(这对于由intervaltimer工厂方法创建的Observable实例并不成立,例如。)这是一件好事,因为处理线程并不那么容易。它们很强大,但它们需要彼此同步;例如,当一个依赖于另一个的结果时。

在多线程环境中最难管理的事情之一是线程之间的共享数据。一个线程可以从数据源中读取,而另一个线程正在修改它,这导致不同版本的相同数据被不同的线程使用。如果Observable链构造得当,就没有共享状态。这意味着同步并不那么复杂。

在本章中,我们将讨论并行执行事务,并了解并发意味着什么。此外,我们将学习一些处理我们的Observable实例发出太多项目的情况的技术(这在多线程环境中并不罕见)。本章涵盖的主题如下:

  • 使用Scheduler实例实现并发

  • 使用Observable实例的缓冲节流去抖动

RxJava 的调度程序

调度程序是 RxJava 实现并发的方式。它们负责为我们创建和管理线程(在内部依赖于 Java 的线程池设施)。我们不会涉及 Java 的并发 API 及其怪癖和复杂性。我们一直在使用调度程序,隐式地使用定时器和间隔,但是现在是掌握它们的时候了。

让我们回顾一下我们在第三章中介绍的Observable.interval工厂方法,创建和连接 Observables、Observers 和 Subjects。正如我们之前看到的,RxJava 默认情况下是单线程的,所以在大多数情况下,在Observable实例上调用subscribe方法会阻塞当前线程。但是interval Observable实例并非如此。如果我们查看Observable<Long> interval(long interval, TimeUnit unit)方法的 JavaDoc,我们会看到它说,由它创建的Observable实例在一个叫做“计算调度程序”的东西上运行。

为了检查interval方法的行为(以及本章中的其他内容),我们将需要一个强大的调试工具。这就是为什么我们在本章中要做的第一件事。

调试 Observables 和它们的调度程序

在上一章中,我们介绍了doOnNext()操作符,它可以用于直接从Observable链中记录发出的项目。我们提到了doOnError()doOnCompleted()操作符。但是有一个结合了所有三者的操作符——doOnEach()操作符。我们可以从中记录所有内容,因为它接收所有发出的通知,而不管它们的类型。我们可以将它放在操作符链的中间,并使用它来记录状态。它接受一个Notification -> void函数。

这是一个返回lambda结果的高阶debug函数的源代码,它能够记录使用传递的描述标记的Observable实例的发射:

<T> Action1<Notification<? super T>> debug(
  String description, String offset
) {
  AtomicReference<String> nextOffset = new AtomicReference<String>(">");
  return (Notification<? super T> notification) -> {
    switch (notification.getKind()) {
    case OnNext:
      System.out.println(
        Thread.currentThread().getName() +
        "|" + description + ": " + offset +
        nextOffset.get() + notification.getValue()
      );
      break;
    case OnError:
      System.err.println(
        Thread.currentThread().getName() +
        "|" + description + ": " + offset +
        nextOffset.get() + " X " + notification.getThrowable()
      );
      break;
    case OnCompleted:
      System.out.println(
        Thread.currentThread().getName() +
        "|" + description + ": " + offset +
        nextOffset.get() + "|"
      );
    default:
      break;
    }
    nextOffset.getAndUpdate(p -> "-" + p);
  };
}

根据传递的descriptionoffset,返回的方法记录每个通知。然而,重要的是,在一切之前记录当前活动线程的名称。<value>标记OnNext 通知X标记OnError 通知|标记OnCompleted 通知nextOffset变量用于显示时间上的值。

这是使用这个新方法的一个例子:

Observable
  .range(5, 5)
  .doOnEach(debug("Test", ""))
  .subscribe();

这个例子将生成五个连续的数字,从数字五开始。我们通过调用我们的debug(String, String)方法传递给doOnEach()操作符来记录range()方法调用之后的一切。通过不带参数的订阅调用,这个小链将被触发。输出如下:

main|Test: >5
main|Test: ->6
main|Test: -->7
main|Test: --->8
main|Test: ---->9
main|Test: ----->|

首先记录的是当前线程的名称(主线程),然后是传递给debug()方法的Observable实例的描述,之后是一个冒号和破折号形成的箭头,表示时间。最后是通知类型的符号——对于值本身是值,对于完成是|

让我们定义debug()辅助方法的一个重载,这样我们就不需要传递第二个参数给它,如果不需要额外的偏移量:

<T> Action1<Notification<? super T>> debug(String description) {
  return debug(description, "");
}

注意

前面方法的代码可以在以下链接查看/下载:github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/common/Helpers.java

现在我们准备调试由间隔方法创建的Observable实例发生了什么!

间隔 Observable 及其默认调度程序

让我们来看下面的例子:

Observable
  .take(5)
  .interval(500L, TimeUnit.MILLISECONDS)
  .doOnEach(debug("Default interval"))
  .subscribe();

这创建了一个interval Observable实例,每隔半秒发出一次。我们使用take()方法只获取前五个通知并完成。我们将使用我们的debug()辅助方法记录由间隔方法创建的Observable实例发出的值,并使用subscribe()调用来触发逻辑。输出应该如下所示:

RxComputationThreadPool-1|Default interval: >0
RxComputationThreadPool-1|Default interval: ->1
RxComputationThreadPool-1|Default interval: -->2
RxComputationThreadPool-1|Default interval: --->3
RxComputationThreadPool-1|Default interval: ---->4

这里应该都很熟悉,除了Observable实例执行的线程!这个线程不是线程。看起来它是由 RxJava 管理的可重用Thread实例池创建的,根据它的名称(RxComputationThreadPool-1)。

如果你还记得,Observable.interval工厂方法有以下重载:

Observable<Long> interval(long, TimeUnit, Scheduler)

这意味着我们可以指定它将在哪个调度程序上运行。之前提到过,只有两个参数的重载在computation调度程序上运行。所以,现在让我们尝试传递另一个调度程序,看看会发生什么:

Observable
  .take(5)
  .interval(500L, TimeUnit.MILLISECONDS, Schedulers.immediate())
  .doOnEach(debug("Imediate interval"))
  .subscribe();

这与以前相同,但有一点不同。我们传递了一个名为immediate的调度程序。这样做的想法是立即在当前运行的线程上执行工作。结果如下:

main|Imediate interval: >0
main|Imediate interval: ->1
main|Imediate interval: -->2
main|Imediate interval: --->3
main|Imediate interval: ---->4

通过指定这个调度程序,我们使interval Observable实例在当前线程上运行。

注意

前面例子的源代码可以在以下链接找到:github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter06/IntervalAndSchedulers.java

借助调度程序的帮助,我们可以指示我们的操作符在特定线程上运行或使用特定的线程池。

我们刚刚讨论的一切都导致了这样的结论:调度程序会生成新的线程,或者重用已经生成的线程,操作Observable实例链的一部分,会在这些线程上执行。因此,我们可以通过仅使用它们来实现并发(操作同时进行)。

为了拥有多线程逻辑,我们只需要学习这两件事:

  • 我们可以选择的调度程序类型

  • 如何在任意Observable链的操作中使用这些调度程序

调度程序的类型

有几种专门用于某种类型操作的schedulers。为了更多地了解它们,让我们看一下Scheduler类。

事实证明这个类非常简单。它只有两个方法,如下所示:

  • long now()

  • abstract Worker createWorker()

第一个返回当前时间的毫秒数,第二个创建一个Worker实例。这些Worker实例用于在单个线程或事件循环上执行操作(取决于实现)。使用Workerschedule*方法来安排执行操作。Worker类实现了Subscription接口,因此它有一个unsubscribe()方法。取消订阅Worker取消排队的所有未完成工作,并允许资源清理。

我们可以使用工作线程在Observable上下文之外执行调度。对于每种Scheduler类型,我们可以做到以下几点:

scheduler.createWorker().schedule(Action0);

这将安排传递的操作并执行它。在大多数情况下,这个方法不应该直接用于调度工作,我们只需选择正确的调度程序并在其上安排操作即可。为了了解它们的作用,我们可以使用这个方法来检查各种可用的调度程序类型。

让我们定义一个测试方法:

void schedule(Scheduler scheduler, int numberOfSubTasks, boolean onTheSameWorker) {
  List<Integer> list = new ArrayList<>(0);
  AtomicInteger current = new AtomicInteger(0);
  Random random = new Random();
  Worker worker = scheduler.createWorker();
  Action0 addWork = () -> {
    synchronized (current) {
      System.out.println("  Add : " + Thread.currentThread().getName() + " " + current.get());
      list.add(random.nextInt(current.get()));
      System.out.println("  End add : " + Thread.currentThread().getName() + " " + current.get());
    }
  };
  Action0 removeWork = () -> {
    synchronized (current) {
      if (!list.isEmpty()) {
        System.out.println("  Remove : " + Thread.currentThread().getName());
        list.remove(0);
        System.out.println("  End remove : " + Thread.currentThread().getName());
      }
    }
  };
  Action0 work = () -> {
    System.out.println(Thread.currentThread().getName());
    for (int i = 1; i <= numberOfSubTasks; i++) {
      current.set(i);
      System.out.println("Begin add!");
      if (onTheSameWorker) {
        worker.schedule(addWork);
      }
      else {
 scheduler.createWorker().schedule(addWork);
      }
      System.out.println("End add!");
    }
    while (!list.isEmpty()) {
      System.out.println("Begin remove!");
    if (onTheSameWorker) {
 worker.schedule(removeWork);
    }
    else {
 scheduler.createWorker().schedule(removeWork);
    }
    System.out.println("End remove!");
  };
  worker.schedule(work);
}

该方法使用传递的Scheduler实例来执行一些工作。有一个选项可以指定它是否应该为每个任务使用相同的Worker实例,或者为每个子任务生成一个新的Worker实例。基本上,虚拟工作包括用随机数填充列表,然后逐个删除这些数字。每个添加操作删除操作都是通过传递的Scheduler实例创建的工作线程作为子任务进行调度的。在每个子任务之前和之后,当前线程和一些额外信息都被记录下来。

提示

在现实世界的场景中,一旦所有工作都完成了,我们应该始终调用worker.unsubscribe()方法。

转向预定义的Scheduler实例。它们可以通过Schedulers类中包含的一组静态方法来获取。我们将使用之前定义的调试方法来检查它们的行为,以了解它们的差异和用处。

Schedulers.immediate调度程序

Schedulers.immediate调度程序在此时此刻执行工作。当一个操作传递给它的工作线程的schedule(Action0)方法时,它就会被调用。假设我们用它来运行我们的测试方法,就像这样:

schedule(Schedulers.immediate(), 2, false);
schedule(Schedulers.immediate(), 2, true);

在这两种情况下,结果看起来都是这样的:

main
Begin add!
 Add : main 1
 End add : main 1
End add!
Begin add!
 Add : main 2
 End add : main 2
End add!
Begin remove!
 Remove : main
 End remove : main
End remove!
Begin remove!
 Remove : main
 End remove : main
End remove!

换句话说,一切都在调用线程上执行——主线程上,没有任何并行操作。

这个调度程序可以用来在前台执行interval()timer()等方法。

Schedulers.trampoline调度程序

通过Schedulers.trampoline方法检索到的调度程序会在当前线程排队子任务。排队的工作会在当前正在进行的工作完成后执行。假设我们要运行这个:

schedule(Schedulers.trampoline(), 2, false);
schedule(Schedulers.trampoline(), 2, true);

在第一种情况下,结果将与立即调度程序相同,因为所有任务都是在它们自己的Worker实例中执行的,因此每个工作线程只有一个任务要排队执行。但是当我们使用相同的Worker实例来调度每个子任务时,我们会得到这样的结果:

main
Begin add!
End add!
Begin add!
End add!
 Add : main 2
 End add : main 2
 Add : main 2
 End add : main 2

换句话说,它将首先执行整个主要操作,然后执行子任务;因此,List实例将被填充(子任务已入队),但永远不会被清空。这是因为在执行主任务时,List实例仍然为空,并且while循环没有被触发。

注意

trampoline调度程序可用于在递归运行多个任务时避免StackOverflowError异常。例如,假设一个任务完成后调用自身执行一些新工作。在单线程环境中,这将导致由于递归而导致堆栈溢出;但是,如果我们使用trampoline调度程序,它将序列化所有已安排的活动,并且堆栈深度将保持正常。但是,trampoline调度程序通常比immediate调度程序慢。因此,使用正确的调度程序取决于用例。

Schedulers.newThread 调度程序

此调度程序为每个新的Worker实例创建一个new Thread实例(确切地说是单线程的ScheduledThreadPoolExecutor实例)。此外,每个工作人员通过其schedule()方法排队接收到的操作,就像trampoline调度程序一样。让我们看看以下代码:

schedule(Schedulers.newThread(), 2, true);

它将具有与trampoline相同的行为,但将在新的thread:中运行:

RxNewThreadScheduler-1
Begin add!
End add!
Begin add!
End add!
  Add : RxNewThreadScheduler-1 2
  End add : RxNewThreadScheduler-1 2
  Add : RxNewThreadScheduler-1 2
  End add : RxNewThreadScheduler-1 2

相反,如果我们像这样调用测试方法:

schedule(Schedulers.newThread(), 2, false);

这将为每个子任务生成一个新的Thread实例,其输出类似于这样:

RxNewThreadScheduler-1
Begin add!
End add!
Begin add!
  Add : RxNewThreadScheduler-2 1
  End add : RxNewThreadScheduler-2 2
End add!
Begin remove!
  Add : RxNewThreadScheduler-3 2
  End add : RxNewThreadScheduler-3 2
End remove!
Begin remove!
End remove!
Begin remove!
  Remove : RxNewThreadScheduler-5
  End remove : RxNewThreadScheduler-5
  Remove : RxNewThreadScheduler-4
  End remove : RxNewThreadScheduler-4
End remove!

通过使用new thread Scheduler实例,您可以执行后台任务。

注意

这里非常重要的要求是,其工作人员需要取消订阅以避免泄漏线程和操作系统资源。请注意,每次创建新线程都是昂贵的,因此在大多数情况下,应使用computationIO Scheduler实例。

Schedulers.computation 调度程序

计算调度程序与new thread调度程序非常相似,但它考虑了运行它的机器的处理器/核心数量,并使用可以重用有限数量线程的线程池。每个新的Worker实例在其中一个Thread实例上安排顺序操作。如果线程当前未被使用,并且它是活动的,则它们将被排队以便稍后执行。

如果我们使用相同的Worker实例,我们将只是将所有操作排队到其线程上,并且结果将与使用一个Worker实例调度,使用new thread Scheduler实例相同。

我的机器有四个核心。假设我像这样调用测试方法:

schedule(Schedulers.computation(), 5, false);

我会得到类似于这样的输出:

RxComputationThreadPool-1
Begin add!
  Add : RxComputationThreadPool-2 1
  End add : RxComputationThreadPool-2 1
End add!
Begin add!
End add!
Begin add!
  Add : RxComputationThreadPool-3 3
  End add : RxComputationThreadPool-3 3
End add!
Begin add!
  Add : RxComputationThreadPool-4 4
End add!
Begin add!
  End add : RxComputationThreadPool-4 4
End add!
Begin remove!
End remove!
Begin remove!
  Add : RxComputationThreadPool-2 5
  End add : RxComputationThreadPool-2 5
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
  Remove : RxComputationThreadPool-3
End remove!
Begin remove!
  End remove : RxComputationThreadPool-3
  Remove : RxComputationThreadPool-2
End remove!
Begin remove!
  End remove : RxComputationThreadPool-2
End remove!
Begin remove!
  Remove : RxComputationThreadPool-2
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
End remove!
Begin remove!
  End remove : RxComputationThreadPool-2
End remove!
  Remove : RxComputationThreadPool-2
Begin remove!
  End remove : RxComputationThreadPool-2
End remove!
  Add : RxComputationThreadPool-1 5
  End add : RxComputationThreadPool-1 5
  Remove : RxComputationThreadPool-1
  End remove : RxComputationThreadPool-1

所有内容都是使用来自池中的四个Thread实例执行的(请注意,有一种方法可以将Thread实例的数量限制为少于可用处理器数量)。

computation Scheduler实例是执行后台工作 - 计算或处理的最佳选择,因此它的名称。您可以将其用于应该在后台运行且不是IO相关或阻塞操作的所有内容。

Schedulers.io 调度程序

输入输出(IO)调度程序使用ScheduledExecutorService实例从线程池中检索线程以供其工作人员使用。未使用的线程将被缓存并根据需要重用。如果需要,它可以生成任意数量的线程。

同样,如果我们只使用一个Worker实例运行我们的示例,操作将被排队到其线程上,并且其行为将与computationnew thread调度程序相同。

假设我们使用多个Worker实例运行它,如下所示:

schedule(Schedulers.io(), 2, false);

它将根据需要从其生成Thread实例。结果如下:

RxCachedThreadScheduler-1
Begin add!
End add!
Begin add!
 Add : RxCachedThreadScheduler-2 2
 End add : RxCachedThreadScheduler-2 2
End add!
Begin remove!
 Add : RxCachedThreadScheduler-3 2
 End add : RxCachedThreadScheduler-3 2
End remove!
Begin remove!
 Remove : RxCachedThreadScheduler-4
 End remove : RxCachedThreadScheduler-4
End remove!
Begin remove!
End remove!
Begin remove!
 Remove : RxCachedThreadScheduler-6
 End remove : RxCachedThreadScheduler-6
End remove!

IO调度程序专用于阻塞IO 操作。用于向服务器发出请求,从文件和套接字读取以及其他类似的阻塞任务。请注意,其线程池是无界的;如果其工作人员未取消订阅,则池将无限增长。

注意

所有前述代码的源代码位于github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter06/SchedulersTypes.java

Schedulers.from(Executor)方法

这可以用来创建一个自定义的Scheduler实例。如果没有预定义的调度程序适合您,可以使用这个方法,将它传递给java.util.concurrent.Executor实例,以实现您需要的行为。

现在我们已经了解了预定义的Scheduler实例应该如何使用,是时候看看如何将它们与我们的Observable序列集成了。

组合 Observable 和调度程序

为了在其他线程上执行我们的可观察逻辑,我们可以使用调度程序。有两个特殊的操作符,它们接收Scheduler作为参数,并生成Observable实例,能够在与当前线程不同的Thread实例上执行操作。

Observable subscribeOn(Scheduler)方法

subscribeOn()方法创建一个Observable实例,其subscribe方法会导致订阅在从传递的调度程序中检索到的线程上发生。例如,我们有这样的:

Observable<Integer> range = Observable
  .range(20, 4)
  .doOnEach(debug("Source"));
range.subscribe();

System.out.println("Hey!");

我们将得到以下输出:

main|Source: >20
main|Source: ->21
main|Source: -->22
main|Source: --->23
main|Source: -------->|
Hey!

这是正常的;调用subscribe方法会在主线程上执行可观察逻辑,只有在所有这些都完成之后,我们才会看到'Hey!'

让我们修改代码看起来像这样:

CountDownLatch latch = new CountDownLatch(1);
Observable<Integer> range = Observable
  .range(20, 4)
  .doOnEach(debug("Source"))
  .subscribeOn(Schedulers.computation())
  .finallyDo(() -> latch.countDown());
range.subscribe();
System.out.println("Hey!");
latch.await();

输出变成了以下内容:

Hey!
RxComputationThreadPool-1|Source: >20
RxComputationThreadPool-1|Source: ->21
RxComputationThreadPool-1|Source: -->22
RxComputationThreadPool-1|Source: --->23
RxComputationThreadPool-1|Source:--------->|

这意味着调用者线程不会阻塞首先打印'Hey!'或在数字之间,所有Observable实例的可观察逻辑都在计算线程上执行。这样,您可以使用任何您喜欢的调度程序来决定在哪里执行工作。

在这里,我们需要提到subscribeOn()方法的一些重要内容。如果您在整个链中多次调用它,就像这样:

CountDownLatch latch = new CountDownLatch(1);
Observable<Integer> range = Observable
  .range(20, 3)
  .doOnEach(debug("Source"))
  .subscribeOn(Schedulers.computation());
Observable<Character> chars = range
  .map(n -> n + 48)
  .map(n -> Character.toChars(n))
  .subscribeOn(Schedulers.io())
  .map(c -> c[0])
  .subscribeOn(Schedulers.newThread())
  .doOnEach(debug("Chars ", "    "))
  .finallyDo(() -> latch.countDown());
chars.subscribe();
latch.await();

调用它时最接近链的开头很重要。在这里,我们首先在计算调度程序上订阅,然后在IO调度程序上,然后在新线程调度程序上,但我们的代码将在计算调度程序上执行,因为这在链中首先指定。

RxComputationThreadPool-1|Source: >20
RxComputationThreadPool-1|Chars :     >D
RxComputationThreadPool-1|Source: ->21
RxComputationThreadPool-1|Chars :     ->E
RxComputationThreadPool-1|Source: -->22
RxComputationThreadPool-1|Chars :     -->F
RxComputationThreadPool-1|Source: --->|
RxComputationThreadPool-1|Chars :     --->|

总之,在生成Observable实例的方法中不要指定调度程序;将这个选择留给方法的调用者。或者,使您的方法接收Scheduler实例作为参数;例如Observable.interval方法。

注意

subscribeOn()操作符可用于在订阅时阻塞调用者线程的Observable实例。在这些源上使用subscribeOn()方法让调用者线程与Observable实例逻辑并发进行。

那么另一个操作符呢,它帮助我们在其他线程上执行工作呢?

Observable observeOn(Scheduler)操作符

observeOn()操作符类似于subscribeOn()操作符,但它不是在传递的Scheduler实例上执行整个链,而是从其在其中的位置开始执行链的一部分。通过一个例子最容易理解这一点。让我们使用稍微修改过的前一个例子:

CountDownLatch latch = new CountDownLatch(1);
Observable<Integer> range = Observable
  .range(20, 3)
  .doOnEach(debug("Source"));
Observable<Character> chars = range
  .map(n -> n + 48)
  .doOnEach(debug("+48 ", "    "))
  .map(n -> Character.toChars(n))
  .map(c -> c[0])
  .observeOn(Schedulers.computation())
  .doOnEach(debug("Chars ", "    "))
  .finallyDo(() -> latch.countDown());
chars.subscribe();
System.out.println("Hey!");
latch.await();

在这里,我们告诉Observable链在订阅后在线程上执行,直到它到达observeOn()操作符。在这一点上,它被移动到计算调度程序上。这样的输出类似于以下内容:

main|Source: >20
main|+48 :     >68
main|Source: ->21
main|+48 :     ->69
main|Source: -->22
main|+48 :     -->70
RxComputationThreadPool-3|Chars :     >D
RxComputationThreadPool-3|Chars :     ->E
RxComputationThreadPool-3|Chars :     -->F
main|Source: --->|
main|+48 :    --->|
Hey!
RxComputationThreadPool-3|Chars :    --->|

正如我们所看到的,调用操作符之前的链部分会阻塞线程,阻止打印Hey!。然而,在所有通知通过observeOn()操作符之后,'Hey!'被打印出来,执行继续在计算线程上进行。

如果我们将observeOn()操作符移到Observable链上,更大部分的逻辑将使用计算调度程序执行。

当然,observeOn()操作符可以与subscribeOn()操作符一起使用。这样,链的一部分可以在一个线程上执行,而其余部分可以在另一个线程上执行(在大多数情况下)。如果你编写客户端应用程序,这是特别有用的,因为通常这些应用程序在一个事件排队线程上运行。你可以使用subscribeOn()/observeOn()操作符使用IO调度程序从文件/服务器读取数据,然后在事件线程上观察结果。

提示

有一个 RxJava 的 Android 模块没有在本书中涵盖,但它受到了很多关注。你可以在这里了解更多信息:github.com/ReactiveX/RxJava/wiki/The-RxJava-Android-Module

如果你是 Android 开发人员,不要错过它!

SwingJavaFx也有类似的模块。

让我们看一个使用subscribeOn()observeOn()操作符的示例:

CountDownLatch latch = new CountDownLatch(1);
Observable<Integer> range = Observable
  .range(20, 3)
  .subscribeOn(Schedulers.newThread())
  .doOnEach(debug("Source"));
Observable<Character> chars = range
  .observeOn(Schedulers.io())
  .map(n -> n + 48)
  .doOnEach(debug("+48 ", "    "))
  .observeOn(Schedulers.computation())
  .map(n -> Character.toChars(n))
  .map(c -> c[0])
  .doOnEach(debug("Chars ", "    "))
  .finallyDo(() -> latch.countDown());
chars.subscribe();
latch.await();

在这里,我们在链的开头使用了一个subsribeOn()操作符的调用(实际上,放在哪里都无所谓,因为它是对该操作符的唯一调用),以及两个observeOn()操作符的调用。执行此代码的结果如下:

RxNewThreadScheduler-1|Source: >20
RxNewThreadScheduler-1|Source: ->21
RxNewThreadScheduler-1|Source: -->22
RxNewThreadScheduler-1|Source: --->|
RxCachedThreadScheduler-1|+48 :     >68
RxCachedThreadScheduler-1|+48 :     ->69
RxCachedThreadScheduler-1|+48 :     -->70
RxComputationThreadPool-3|Chars :     >D
RxCachedThreadScheduler-1|+48 :     --->|
RxComputationThreadPool-3|Chars :     ->E
RxComputationThreadPool-3|Chars :     -->F
RxComputationThreadPool-3|Chars :     --->|

我们可以看到链通过了三个线程。如果我们使用更多元素,一些代码将看起来是并行执行的。结论是,使用observeOn()操作符,我们可以多次更改线程;使用subscribeOn()操作符,我们可以一次性进行此操作—订阅

注意

使用observeOn()/subscribeOn()操作符的上述示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter06/SubscribeOnAndObserveOn.java找到。

使用这两个操作符,我们可以让Observable实例和多线程一起工作。但是并发并不真正意味着我们可以并行执行任务。它意味着我们的程序有多个线程,可以独立地取得一些进展。真正的并行是当我们的程序以最大限度利用主机机器的 CPU(核心)并且其线程实际上同时运行时。

到目前为止,我们的所有示例都只是将链逻辑移动到其他线程上。尽管有些示例确实在并行中执行了部分操作,但真正的并行示例看起来是不同的。

并行

我们只能通过使用我们已经知道的操作符来实现并行。想想flatMap()操作符;它为源发出的每个项目创建一个Observable实例。如果我们在这些Observable实例上使用subscribeOn()操作符和Scheduler实例,每个实例将在新的Worker实例上调度,并且它们将并行工作(如果主机机器允许)。这是一个例子:

Observable<Integer> range = Observable
  .range(20, 5)
  .flatMap(n -> Observable
    .range(n, 3)
    .subscribeOn(Schedulers.computation())
    .doOnEach(debug("Source"))
  );
range.subscribe();

这段代码的输出如下:

RxComputationThreadPool-3|Source: >23
RxComputationThreadPool-4|Source: >20
RxComputationThreadPool-2|Source: >22
RxComputationThreadPool-3|Source: ->24
RxComputationThreadPool-1|Source: >21
RxComputationThreadPool-2|Source: ->23
RxComputationThreadPool-3|Source: -->25
RxComputationThreadPool-3|Source: --->|
RxComputationThreadPool-4|Source: ->21
RxComputationThreadPool-4|Source: -->22
RxComputationThreadPool-4|Source: --->|
RxComputationThreadPool-2|Source: -->24
RxComputationThreadPool-2|Source: --->|
RxComputationThreadPool-1|Source: ->22
RxComputationThreadPool-1|Source: -->23
RxComputationThreadPool-1|Source: --->|
RxComputationThreadPool-4|Source: >24
RxComputationThreadPool-4|Source: ->25
RxComputationThreadPool-4|Source: -->26
RxComputationThreadPool-4|Source: --->|

我们可以通过线程的名称看出,通过flatMap()操作符定义的Observable实例是在并行中执行的。这确实是这种情况——四个线程正在使用我的处理器的四个核心。

我将提供另一个示例,这次是对远程服务器进行并行请求。我们将使用前一章中定义的requestJson()方法。思路是这样的:

  1. 我们将检索 GitHub 用户的关注者信息(在本例中,我们将使用我的帐户)。

  2. 对于每个关注者,我们将得到其个人资料的 URL。

  3. 我们将以并行方式请求关注者的个人资料。

  4. 我们将打印关注者的数量以及他们的关注者数量。

让我们看看这是如何实现的:

Observable<Map> response = CreateObservable.requestJson(
  client,
  "https://api.github.com/users/meddle0x53/followers"
); // (1)
response
  .map(followerJson -> followerJson.get("url")) // (2)
  .cast(String.class)
  .flatMap(profileUrl -> CreateObservable
    .requestJson(client, profileUrl)
    .subscribeOn(Schedulers.io()) // (3)
    .filter(res -> res.containsKey("followers"))
    .map(json ->  // (4)
      json.get("login") +  " : " +
      json.get("followers"))
  )
  .doOnNext(follower -> System.out.println(follower)) // (5)
  .count() // (6)
  .subscribe(sum -> System.out.println("meddle0x53 : " + sum));

在上述代码中发生了什么:

  1. 首先,我们对我的用户的关注者数据进行请求。

  2. 请求以JSON字符串形式返回关注者,这些字符串被转换为Map对象(请参阅requestJson方法的实现)。从每个JSON文件中,读取表示关注者个人资料的 URL。

  3. 对每个 URL 执行一个新的请求。请求在IO线程上并行运行,因为我们使用了与前面示例相同的技术。值得一提的是,flatMap()运算符有一个重载,它接受一个maxConcurrent整数参数。我们可以使用它来限制并发请求。

  4. 获取关注者的用户数据后,生成他/她的关注者的信息。

  5. 这些信息作为副作用打印出来。

  6. 使用count()运算符来计算我的关注者数量(这与scan(0.0, (sum, element) -> sum + 1).last()调用相同)。然后我们打印它们。打印的数据顺序不能保证与遍历关注者的顺序相同。

注意

前面示例的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter06/ParallelRequestsExample.java找到。

这就是并发并行的全部内容。一切都很简单,但功能强大。有一些规则(例如使用Subscribers.io实例进行阻塞操作,使用计算实例进行后台任务等),您必须遵循以确保没有任何问题,即使是多线程的可观察链操作。

使用这种并行技术很可能会使Observable实例链中涌入大量数据,这是一个问题。这就是为什么我们必须处理它。在本章的其余部分,我们将学习如何处理来自上游可观察链操作的太多元素。

缓冲、节流和去抖动

这里有一个有趣的例子:

Path path = Paths.get("src", "main", "resources");
Observable<String> data = CreateObservable
  .listFolder(path, "*")
  .flatMap(file -> {
    if (!Files.isDirectory(file)) {
      return CreateObservable
    .from(file)
    .subscribeOn(Schedulers.io());
  }
  return Observable.empty();
});
subscribePrint(data, "Too many lines");

这将遍历文件夹中的所有文件,并且如果它们本身不是文件夹,则会并行读取它们。例如,当我运行它时,文件夹中有五个文本文件,其中一个文件相当大。在使用我们的subscribePrint()方法打印这些文件的内容时,我们得到了类似于这样的内容:

Too many lines : Morbi nec nulla ipsum.
Too many lines : Proin eu tellus tortor.
Too many lines : Lorem ipsum dolor sit am
Error from Too many lines:
rx.exceptions.MissingBackpressureException
Too many lines : Vivamus non vulputate tellus, at faucibus nunc.
Too many lines : Ut tristique, orci eu
Too many lines : Aliquam egestas malesuada mi vitae semper.
Too many lines : Nam vitae consectetur risus, vitae congue risus.
Too many lines : Donec facilisis sollicitudin est non molestie.
 rx.internal.util.RxRingBuffer.onNext(RxRingBuffer.java:349)
 rx.internal.operators.OperatorMerge$InnerSubscriber.enqueue(OperatorMerge.java:721)
 rx.internal.operators.OperatorMerge$InnerSubscriber.emit(OperatorMerge.java:698)
 rx.internal.operators.OperatorMerge$InnerSubscriber.onNext(OperatorMerge.java:586)
 rx.internal.operators.OperatorSubscribeOn$1$1$1.onNext(OperatorSubscribeOn.java:76)

输出被裁剪了,但重要的是我们得到了MissingBackpressureException异常。

读取每个文件的线程正在尝试将它们的数据推送到merge()运算符(flatMap()运算符实现为merge(map(func)))。该运算符正在努力处理大量数据,因此它将尝试通知过度生产的Observable实例减速(通知上游无法处理数据量的能力称为背压)。问题在于它们没有实现这样的机制(背压),因此会遇到MissingBackpressureException异常。

通过在上游可观察对象中实现背压,使用其中一个特殊的onBackpressure*方法或尝试通过将大量传入的项目打包成更小的发射集来避免它。这种打包是通过缓冲丢弃一些传入的项目、节流(使用时间间隔或事件进行缓冲)和去抖动(使用项目发射之间的间隔进行缓冲)来完成的。

让我们检查其中一些。

节流

使用这种机制,我们可以调节Observable实例的发射速率。我们可以指定时间间隔或另一个流控制Observable实例来实现这一点。

使用sample()运算符,我们可以使用另一个Observable实例或时间间隔来控制Observable实例的发射。

data = data
  .sample(
 Observable
 .interval(100L, TimeUnit.MILLISECONDS)
 .take(10)
 .concatWith(
 Observable
 .interval(200L, TimeUnit.MILLISECONDS)
 )
 );
subscribePrint(data, "Too many lines");

采样 Observable 实例在前两秒每 100 毫秒发出一次,然后开始每 200 毫秒发出一次。data Observable 实例放弃了所有项目,直到 sampling 发出。当这种情况发生时,data Observable 实例发出的最后一个项目被传递。因此,我们有很大的数据丢失,但更难遇到 MissingBackpressureException 异常(尽管有可能遇到)。

sample() 操作符有两个额外的重载,可以传递时间间隔、TimeUnit 度量和可选的 Scheduler 实例:

data = data.sample(
 100L,
 TimeUnit.MILLISECONDS
);

使用 sample() 操作符与 Observable 实例可以更详细地控制数据流。throttleLast() 操作符只是 sample() 操作符的不同版本的别名,它接收时间间隔。throttleFirst() 操作符与 throttleLast() 操作符相同,但 source Observable 实例将在间隔开始时发出它发出的第一个项目,而不是最后一个。这些操作符默认在 computation 调度程序上运行。

这些技术在有多个相似事件时非常有用(以及本节中的大多数其他技术)。例如,如果您想捕获并对 鼠标移动事件 做出反应,您不需要包含所有像素位置的所有事件;您只需要其中一些。

防抖动

在我们之前的例子中,防抖动 不起作用。它的想法是仅发出在给定时间间隔内没有后续项目的项目。因此,必须在发射之间经过一些时间才能传播一些东西。因为我们 data Observable 实例中的所有项目似乎一次性发出,它们之间没有可用的间隔。因此,我们需要稍微改变示例以演示这一点。

Observable<Object> sampler = Observable.create(subscriber -> {
  try {
    subscriber.onNext(0);
    Thread.sleep(100L);
    subscriber.onNext(10);
    Thread.sleep(200L);
    subscriber.onNext(20);
    Thread.sleep(150L);
    subscriber.onCompleted();
  }
  catch (Exception e) {
    subscriber.onError(e);
  }
}).repeat()
  .subscribeOn(Schedulers.computation());
data = data
  .sample(sampler)
  .debounce(150L, TimeUnit.MILLISECONDS);

在这里,我们使用 sample() 操作符与特殊的 sampling Observable 实例,以便将发射减少到发生在 100、200 和 150 毫秒的时间间隔上。通过使用 repeat() 操作符,我们创建了一个重复源的 无限 Observable 实例,并将其设置为在 computation 调度程序上执行。现在我们可以使用 debounce() 操作符,只发出这组项目,并在它们的发出之间有 150 毫秒或更长的时间间隔。

防抖动,像 节流 一样,可以用于过滤来自过度生产的源的相似事件。一个很好的例子是自动完成搜索。我们不希望在用户输入每个字母时触发搜索;我们需要等待他/她停止输入,然后触发搜索。我们可以使用 debounce() 操作符,并设置一个合理的 时间间隔debounce() 操作符有一个重载,它将 Scheduler 实例作为其第三个参数。此外,还有一个带有选择器返回 Observable 实例的重载,以更精细地控制 数据流

缓冲和窗口操作符

这两组操作符与 map()flatMap() 操作符一样是 transforming 操作符。它们将一系列元素转换为一个集合,这些元素的序列将作为一个元素发出。

本书不会详细介绍这些操作符,但值得一提的是,buffer() 操作符具有能够基于 时间间隔选择器 和其他 Observable 实例收集发射的重载。它还可以配置为跳过项目。以下是使用 buffer(int count, int skip) 方法的示例,这是 buffer() 操作符的一个版本,它收集 count 个项目并跳过 skip 个项目:

data = data.buffer(2, 3000);
Helpers.subscribePrint(data, "Too many lines");

这将输出类似于以下内容:

Too many lines : ["Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Donec facilisis sollicitudin est non molestie."]
Too many lines : ["Integer nec magna ac ex rhoncus imperdiet.", "Nullam pharetra iaculis sem."]
Too many lines : ["Integer nec magna ac ex rhoncus imperdiet.", "Nullam pharetra iaculis sem."]
Too many lines : ["Nam vitae consectetur risus, vitae congue risus.", "Donec facilisis sollicitudin est non molestie."]
Too many lines : ["Sed mollis facilisis rutrum.", "Proin enim risus, congue id eros at, pharetra consectetur ex."]
Too many lines ended!

window() 操作符与 buffer() 操作符具有完全相同的重载集。不同之处在于,window() 操作符创建的 Observable 实例发出发出收集的元素的 Observable 实例,而不是缓冲元素的数组。

为了演示不同的重载,我们将使用window(long timespan, long timeshift, TimeUnit units)方法来举例。该操作符会收集在timespan时间间隔内发出的元素,并跳过在timeshift时间间隔内发出的所有元素。这将重复,直到源Observable实例完成。

data = data
  .window(3L, 200L, TimeUnit.MILLISECONDS)
  .flatMap(o -> o);
subscribePrint(data, "Too many lines");

我们使用flatMap()操作符来展平Observable实例。结果包括在订阅的前三毫秒内发出的所有项,以及在 200 毫秒间隔后的三毫秒内发出的项,这将在源发出时重复。

注意

在前一节介绍的所有示例都可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter06/BackpressureExamples.java找到。

背压操作符

最后一组操作符可以防止MissingBackpressureException异常,当有一个过度生产的Observable实例时,它们会自动激活。

onBackpressureBuffer()操作符会对由快于其Observer实例的Observable发出的项进行缓冲。然后以订阅者可以处理的方式发出缓冲的项。例如:

Helpers.subscribePrint(
  data.onBackpressureBuffer(10000),
  "onBackpressureBuffer(int)"
);

在这里,我们使用了一个大容量的缓冲区,因为元素数量很大,但请注意,溢出此缓冲区将导致MissingBackpressureException异常。

onBackpressureDrop()操作符会丢弃所有来自Observable实例的无法被订阅者处理的传入项。

有一种方法可以通过实现智能的 Observables 或 Subscribers 来建立背压,但这个话题超出了本书的范围。在 RxJava 维基页面上有一篇关于背压和 observable 的优秀文章—github.com/ReactiveX/RxJava/wiki/Backpressure。本节中提到的许多操作符在那里都有详细描述,并且有大理石图可用于帮助您理解更复杂的操作符。

总结

在本章中,我们学习了如何在与线程不同的其他线程上执行我们的 observable 逻辑。有一些简单的规则和技术可以做到这一点,如果一切都按照规定进行,就不应该有危险。使用这些技术,我们能够编写并发程序。我们还学习了如何使用调度程序和flatMap()操作符实现并行执行,并且看到了一个真实世界的例子。

我们还研究了如何处理过度生产的数据源。有许多操作符可以通过不同的方式来做到这一点,我们介绍了其中一些,并讨论了它们的有用性。

有了这些知识,我们可以编写任意的 RxJava 程序,能够处理来自不同源的数据。我们知道如何使用多个线程来做到这一点。使用 RxJava、它的操作符和构造几乎就像使用一种新语言编码。它有自己的规则和流程控制方法。

为了编写稳定的应用程序,我们必须学会如何对它们进行单元测试。测试异步代码并不是一件容易的事情。好消息是,RxJava 提供了一些操作符和类来帮助我们做到这一点。您可以在下一章中了解更多信息。

第七章:测试您的 RxJava 应用程序

在编写软件时,尤其是将被许多用户使用的软件,我们需要确保一切都正常运行。我们可以编写可读性强、结构良好、模块化的代码,这将使更改和维护变得更容易。我们应该编写测试,因为每个功能都存在回归的危险。当我们已经为现有代码编写了测试时,重构它就不会那么困难,因为测试可以针对新的、更改过的代码运行。

几乎一切都需要进行测试和自动化。甚至有一些意识形态,如测试驱动开发TDD)和行为驱动开发BDD)。如果我们不编写自动化测试,我们不断变化的代码往往会随着时间的推移而变得更加难以测试和维护。

在本章中,我们不会讨论为什么需要测试我们的代码。我们将接受这是强制性的,并且是作为程序员生活的一部分。我们将学习如何测试使用 RxJava 编写的代码。

我们将看到编写它的单元测试并不那么困难,但也有一些难以测试的情况,比如异步Observable实例。我们将学习一些新的操作符,这些操作符将帮助我们进行测试,以及一种新的Observable实例。

说到这里,这一章我们将涵盖以下内容:

  • 通过BlockingObservable类和聚合操作测试Observable实例

  • 使用TestSubscriber实例进行深入测试

  • TestScheduler类和测试异步Observable实例

使用简单订阅进行测试

我们可以通过简单订阅Observable实例并收集所有传入的通知来测试我们得到的内容。为了演示这一点,我们将开发一个用于创建新Observable实例并测试其行为的factory方法。

该方法将接收一个Comparator实例和多个项目,并将返回Observable实例,按排序顺序发出这些项目。项目将根据传递的Comparator实例进行排序。

我们可以使用 TDD 来开发这个方法。让我们首先定义测试如下:

public class SortedObservableTest {
  private Observable<String> tested;
  private List<String> expected;
  @Before
  public void before() {
    tested = CreateObservable.<String>sorted(
 (a, b) -> a.compareTo(b),
 "Star", "Bar", "Car", "War", "Far", "Jar");
    expected = Arrays.asList(
      "Bar", "Car", "Far", "Jar", "Star", "War"
    );
  }
  TestData data = new TestData();
  tested.subscribe(
    (v) -> data.getResult().add(v),
    (e) -> data.setError(e),
    () -> data.setCompleted(true)
  );
  Assert.assertTrue(data.isCompleted());
  Assert.assertNull(data.getError());
  Assert.assertEquals(expected, data.getResult());
}

注意

本章的示例使用JUnit框架进行测试。您可以在junit.org了解更多信息。

该测试使用两个变量来存储预定义的可重用状态。第一个是我们用作源的Observable实例—被测试的。在设置@Before方法中,它被分配给我们的方法CreateObservable.sorted(Comparator, T...)的结果,该方法尚未实现。我们比较一组String实例,并期望它们按照预期变量中存储的顺序接收—第二个可重用字段。

测试本身相当冗长。它使用TestData类的一个实例来存储来自被测试Observable实例的通知。

如果有一个OnCompleted通知,data.completed字段将设置为True。我们期望这种情况发生,这就是为什么我们在测试方法的最后进行断言。如果有一个OnError通知,data.error字段将设置为错误。我们不希望发生这种情况,所以我们断言它为null

Observable实例发出的每个传入项目都将添加到data.resultList字段中。最后,它应该等于预期List变量,我们对此进行断言。

注意

前面测试的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/test/java/com/packtpub/reactive/chapter07/SortedObservableTest.java中查看/下载——这是第一个测试方法。

然而,这个测试当然失败了,因为CreateObservable.sorted(Comparator, T...)方法还没有实现。让我们实现它并再次运行测试:

@SafeVarargs
public static <T> Observable<T> sorted(
  Comparator<? super T> comparator,
  T... data) {
    List<T> listData = Arrays.asList(data);
    listData.sort(comparator);
  return Observable.from(listData);
}

就是这么简单!它只是将传递的varargs数组转换为一个List变量,并使用它的sort()方法与传递的Comparator实例对其进行排序。然后,使用Observable.from(Iterable)方法,我们返回所需的Observable实例。

注意

前面实现的源代码可以在以下位置找到:github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/common/CreateObservable.java#L262

如果现在运行测试,它将通过。这很好!我们有了我们的第一个测试!但是编写类似这样的测试需要大量的样板代码。我们总是需要这三个状态变量,我们总是需要断言相同的事情。那么像interval()timer()方法创建的异步Observable实例呢?

有一些技术可以去除样板变量,稍后,我们将看看如何测试异步行为。现在,我们将介绍一种新类型的 observable。

BlockingObservable 类

每个Observable实例都可以用toBlocking()方法转换为BlockingObservable实例。BlockingObservable实例有多个方法,它们会阻塞当前线程,直到Observable实例发出OnCompletedOnError通知。如果有OnError通知,将抛出异常(RuntimeException异常直接抛出,检查异常包装在RuntimeException实例中)。

toBlocking()方法本身不会阻塞,但它返回的BlockingObservable实例的方法可能会阻塞。让我们看一些这些方法:

  • 我们可以使用forEach()方法迭代BlockingObservable实例中的所有项目。这是一个使用的例子:
Observable
  .interval(100L, TimeUnit.MILLISECONDS)
  .take(5)
  .toBlocking()
 .forEach(System.out::println);
System.out.println("END");

这也是如何使异步代码表现同步的一个例子。interval()方法创建的Observable实例不会在后台执行,因为toBlocking()方法使当前线程等待直到它完成。这就是为什么我们在这里使用take(int)方法,否则,线程将永远被阻塞forEach()方法将使用传递的函数打印五个项目,只有在那之后我们才会看到END输出。BlockingObservable类也有一个toIterable()方法。它返回的Iterable实例也可以用于迭代源发出的序列。

  • 有类似异步阻塞方法,比如first()last()firstOrDefault()lastOrDefault()方法(我们在第四章中讨论过它们,转换、过滤和累积您的数据)。它们在等待所需项目时都会阻塞。让我们看一下以下代码片段:
Integer first = Observable
  .range(3, 13).toBlocking().first();
  System.out.println(first);
  Integer last = Observable
  .range(3, 13).toBlocking().last();
  System.out.println(last);

这将打印'3''15'

  • 一个有趣的方法是single()方法;当发出一个项目并且完成时,它只返回一个项目。如果没有发出项目,或者发出多个项目,分别抛出NoSuchElementException异常或IllegalArgumentException异常。

  • 有一个next()方法,它不会阻塞,而是返回一个Iterable实例。当从这个Iterable实例中检索到一个Iterator实例时,它的每个next()方法都会阻塞,同时等待下一个传入的项目。这可以用于无限的Observable实例,因为当前线程只会在等待下一个项目时阻塞,然后它就可以继续了。(请注意,如果没有人及时调用next()方法,源元素可能会被跳过)。这是一个使用的例子:

Iterable<Long> next = Observable
  .interval(100L, TimeUnit.MILLISECONDS)
  .toBlocking()
 .next();
Iterator<Long> iterator = next.iterator();
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());

当前线程阻塞3 次,每次 100 毫秒,然后在每次暂停后打印012。还有一个类似的方法叫做latest(),它返回一个Iterable实例。行为不同,因为latest()方法产生的Iterable实例返回源发出的最后一个项目,或者如果没有,则等待下一个项目。

Iterable<Long> latest = Observable
  .interval(1000L, TimeUnit.MILLISECONDS)
  .toBlocking()
 .latest();
iterator = latest.iterator();
System.out.println(iterator.next());
Thread.sleep(5500L);
System.out.println(iterator.next());
System.out.println(iterator.next());

这将打印0,然后56

注意

展示所有前述运算符以及聚合运算符的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter07/BlockingObservablesAndOperators.java中查看/下载。

使用BlockingObservable实例可以帮助我们收集我们的测试数据。但是还有一组称为聚合运算符Observable运算符,当与BlockingObservables实例结合使用时也很有用。

聚合运算符和 BlockingObservable 类

聚合运算符产生的Observable实例只发出一个项目并完成。这个项目是由source Observable实例发出的所有项目组成或计算得出的。在本节中,我们只讨论其中的两个。有关更详细的信息,请参阅github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators

其中第一个运算符是count()countLong()方法。它发出source Observable实例发出的项目数。例如:

Observable
  .range(10, 100)
  .count()
  .subscribe(System.out::println);

这将打印100

另一个是toList()toSortedList()方法,它发出一个包含source Observable实例发出的所有项目的list变量(可以排序)并完成。

List<Integer> list = Observable
  .range(5, 15)
  .toList()
  .subscribe(System.out::println);

这将输出以下内容:

[5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

所有这些方法,结合toBlocking()方法一起很好地工作。例如,如果我们想要检索由source Observable实例发出的所有项目的列表,我们可以这样做:

List<Integer> single = Observable
  .range(5, 15)
  .toList()
 .toBlocking().single();

我们可以根据需要使用这些项目的集合:例如用于测试。

提示

聚合运算符还包括一个collect()运算符,它可以用于生成Observable实例并发出任意集合,例如Set()运算符。

使用聚合运算符和 BlockingObservable 类进行测试

使用在前两节中学到的运算符和方法,我们能够重新设计我们编写的测试,使其看起来像这样:

@Test
public void testUsingBlockingObservable() {
  List<String> result = tested
    .toList()
 .toBlocking()
 .single();
  Assert.assertEquals(expected, result);
}

这里没有样板代码。我们将所有发出的项目作为列表检索并将它们与预期的列表进行比较。

在大多数情况下,使用BlockingObsevables类和聚合运算符非常有用。然而,在测试异步Observable实例时,它们并不那么有用,因为它们发出长时间的慢序列。长时间阻塞测试用例不是一个好的做法:慢测试是糟糕的测试。

注意

前面测试的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/test/java/com/packtpub/reactive/chapter07/SortedObservableTest.java找到-这是第二个测试方法。

另一个这种测试方法不太有用的情况是当我们想要检查source发送的Notification对象或订阅状态时。

还有一种编写测试的技术,可以更精细地控制订阅本身,这是通过一个特殊的Subscriber-TestSubscriber

使用 TestSubscriber 类进行深入测试

TestSubscriber实例是一个特殊的Subscriber实例,我们可以将其传递给任何Observable实例的subscribe()方法。

我们可以从中检索所有接收到的项目和通知。我们还可以查看接收到通知的最后一个thread和订阅状态。

让我们使用它来重写我们的测试,以展示它的功能和存储的内容:

@Test
public void testUsingTestSubscriber() {
  TestSubscriber<String> subscriber =
 new TestSubscriber<String>();
  tested.subscribe(subscriber);
  Assert.assertEquals(expected, subscriber.getOnNextEvents());
  Assert.assertSame(1, subscriber.getOnCompletedEvents().size());
  Assert.assertTrue(subscriber.getOnErrorEvents().isEmpty());
  Assert.assertTrue(subscriber.isUnsubscribed());
}

测试是非常简单的。我们创建一个TestSubscriber实例,并使用它订阅被测试的Observable实例。在Observable实例完成后,我们可以访问整个状态。让我们来看一下以下的术语列表:

  • 通过getOnNextEvents()方法,我们能够检索Observable实例发出的所有项目,并将它们与expectedList变量进行比较。

  • 通过getOnCompletedEvents()方法,我们能够检查OnCompleted通知,并检查是否已发送。例如,Observable.never()方法不会发送它。

  • 通过getOnErrorEvents()方法,我们能够检查OnError通知是否存在。在这种情况下,我们assert没有errors

  • 使用isUnsubscribed()方法,我们可以assert在一切完成后,我们的Subscriber实例已被unsubscribed

TestSubscriber实例也有一些assertion方法。因此,还有一种测试的方法:

@Test
public void testUsingTestSubscriberAssertions() {
  TestSubscriber<String> subscriber = new TestSubscriber<String>();
  tested.subscribe(subscriber);
 subscriber.assertReceivedOnNext(expected);
 subscriber.assertTerminalEvent();
 subscriber.assertNoErrors();
 subscriber.assertUnsubscribed();
}

这些几乎是相同的assertions,但是使用TestSubscriber实例自己的assert*方法完成。

注意

前面测试的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/test/java/com/packtpub/reactive/chapter07/SortedObservableTest.java找到-这是第三和第四个测试方法。

通过这些技术,我们可以测试RxJava逻辑的不同行为和状态。在本章中还有一件事要学习-测试异步Observable实例,例如Observable.interval()方法创建的实例。

使用 TestScheduler 类测试异步 Observable 实例

在第六章中我们没有提到的最后一种预定义的schedulerTestScheduler调度程序,这是一个专为单元测试设计的scheduler。在它上面安排的所有操作都被包装在对象中,这些对象包含它们应该执行的时间,并且在调用Scheduler实例的triggerActions()方法之前不会执行。这个方法执行所有未执行并且计划在Scheduler实例的当前时间或之前执行的操作。这个时间是虚拟的。这意味着它是由我们设置的,我们可以使用这个scheduler的特殊方法提前到未来的任何时刻。

为了演示它,我们将开发另一种创建新类型的observable的方法。该方法的实现本身不会在本章中讨论,但您可以在附带书籍的源代码中找到它。

该方法创建一个在设定时间间隔发出项目的Observable实例。但是间隔不是均匀分布的,就像内置的interval方法一样。我们可以提供一个不同的多个间隔的列表,Observable实例将无限循环其中。该方法的签名如下:

Observable<Long> interval(List<Long> gaps, TimeUnit unit, Scheduler scheduler)

如果我们传递一个只包含一个时间段值的List变量,它的行为应该与Observable.interval方法相同。以下是针对这种情况的测试:

@Test
public void testBehavesAsNormalIntervalWithOneGap() {
  TestScheduler testScheduler = Schedulers.test(); // (1)
  Observable<Long> interval = CreateObservable.interval(
 Arrays.asList(100L), TimeUnit.MILLISECONDS, testScheduler
 ); // (2)
  TestSubscriber<Long> subscriber = new TestSubscriber<Long>();
  interval.subscribe(subscriber); // (3)
  assertTrue(subscriber.getOnNextEvents().isEmpty()); // (4)
  testScheduler.advanceTimeBy(101L, TimeUnit.MILLISECONDS); // (5)
  assertEquals(Arrays.asList(0L), subscriber.getOnNextEvents());
  testScheduler.advanceTimeBy(101L, TimeUnit.MILLISECONDS); // (6)
  assertEquals(
    Arrays.asList(0L, 1L),
    subscriber.getOnNextEvents()
  );
  testScheduler.advanceTimeTo(1L, TimeUnit.SECONDS); // (7)
  assertEquals(
    Arrays.asList(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L),
    subscriber.getOnNextEvents()
  );
}

让我们来看一下以下的解释:

  1. 我们使用Schedulers.test()方法创建TestScheduler实例。

  2. 我们的方法的第三个参数是一个Scheduler实例。它将在其上发出项目,因此我们传递我们的TestScheduler实例。

  3. 使用TestSubscriber实例,我们订阅Observable实例。

  4. 订阅后立即,我们不应该有任何通知,因此我们要检查一下。

  5. TestScheduler实例有一个advanceTimeBy(long, TimeUnit)方法,它控制其Worker实例的时间,因此我们可以使用它将时间推进 101 毫秒。101 毫秒后,我们期望收到一个项目——0

  6. 使用advanceTimeBy()方法,我们将时间推进 101 毫秒,然后我们应该已经收到了01

  7. TestScheduler实例的另一个重要方法是advanceTimeTo(long, TimeUnit)方法。它可以用来推进到未来的特定时间点。因此,我们使用它来到达从订阅开始过去一秒的时刻。我们期望到那时已经收到了十个通知。

TestScheduler实例使用其advanceTimeBy()advanceTimeTo()方法来控制时间,因此我们不需要阻塞**主Thread实例等待某些事件发生。我们可以直接到达它已经发生的时间。使用TestScheduler实例,有一个全局事件顺序。因此,如果两个任务被安排在完全相同的时间,它们有一个将执行的顺序,并且可能会导致测试出现问题,因为测试期望特定的全局顺序。如果我们有这样的操作符需要测试,我们应该通过定时到不同的值来避免这种情况——一个是 100 毫秒,另一个是 101 毫秒。使用这种技术,测试异步Observable实例不再是一个复杂的任务。

注意

前面测试的源代码可以在以下链接找到:github.com/meddle0x53/learning-rxjava/blob/master/src/test/java/com/packtpub/reactive/chapter07/CreateObservableIntervalTest.java

总结

通过本章,我们不仅了解了如何使用 RxJava 编写程序,还了解了如何测试它们的任何方面。我们还学习了一些新的操作符和BlockingObservables类。

RxJava 库有许多在本书中未提及的操作符,但我们已经学习了更重要和有用的操作符。您可以随时参考github.com/ReactiveX/RxJava/wiki了解其余部分。关于订阅背压Observable实例的生命周期还有更多内容,但是凭借您目前的知识,掌握库中的一切不会很难。请记住,这只是一个库,一个编写代码的工具。逻辑才是重要的。这种编程方式与过程式编程有些不同,但一旦您掌握了它,就会觉得自然。

在下一章中,我们将学习如何释放订阅分配的资源,如何防止内存泄漏,以及如何创建我们自己的操作符,这些操作符可以在RxJava逻辑中链接。

第八章:资源管理和扩展 RxJava

通过前面的章节,我们已经学会了如何使用 RxJava 的可观察对象。我们已经使用了许多不同的操作符和工厂方法。工厂方法是各种具有不同行为和发射源的Observable实例的来源。另一方面,使用操作符,我们已经围绕这些可观察对象构建了复杂的逻辑。

在本章中,我们将学习如何创建我们自己的工厂方法,这些方法将能够管理它们的源资源。为了做到这一点,我们需要一种管理和释放资源的方法。我们已经创建并使用了多种类似的方法,包括源文件、HTTP 请求、文件夹或内存中的数据。但其中一些并没有清理它们的资源。例如,HTTP 请求可观察对象需要一个CloseableHttpAsyncClient实例;我们创建了一个接收它并将其管理留给用户的方法。现在是时候学习如何自动管理和清理我们的源数据,封装在我们的工厂方法中了。

我们也将学习如何编写我们自己的操作符。Java 不是一种动态语言,这就是为什么我们不会将操作符添加为Observable类的方法。有一种方法可以将它们插入到可观察的操作链中,我们将在本章中看到。

本章涵盖的主题有:

  • 使用using()方法进行资源管理

  • 使用高阶 lift() 操作符创建自定义操作符

  • 使用compose创建操作符的组合

资源管理

如果我们回顾一下我们在第六章中使用的 HTTP 请求方法,使用调度程序进行并发和并行处理和第五章中使用的 HTTP 请求方法,它的签名是:Observable<Map> requestJson(HttpAsyncClient client, String url)

我们不仅仅是调用一个方法,该方法向 URL 发出请求并将响应作为 JSON 返回,我们创建了一个HttpAsyncClient实例,必须启动它并将其传递给requestJson()方法。但还有更多:我们需要在读取结果后关闭客户端,因为可观察是异步的,我们需要等待它的OnCompleted通知,然后关闭它。这非常复杂,应该进行更改。从文件中读取的Observable需要在所有订阅者取消订阅后创建流/读取器/通道并关闭它们。从数据库中发出数据的Observable应该在读取完成后设置并关闭所有连接、语句和结果集。对于HttpAsyncClient对象也是如此。它是我们用来打开与远程服务器的连接的资源;我们的可观察对象应该在一切都被读取并且所有订阅者不再订阅时清理它。

让我们回答一个问题:为什么requestJson()方法需要这个HttpAsyncClient对象?答案是我们使用了一个 RxJava 模块进行 HTTP 请求。其代码如下:

ObservableHttp
  .createGet(url, client)
  .toObservable();

这段代码创建了请求,代码需要客户端,所以我们需要客户端来创建我们的Observable实例。我们不能改变这段代码,因为改变它意味着要自己编写 HTTP 请求,这样不好。已经有一个库可以为我们做这件事。我们需要使用一些东西,在订阅时提供HttpAsyncClient实例,并在取消订阅时释放它。有一个方法可以做到这一点:using()工厂方法。

介绍Observable.using方法

Observable.using方法的签名如下:

public final static <T, Resource> Observable<T> using(
  final Func0<Resource> resourceFactory,
  final Func1<? super Resource, ? extends Observable<? extends T>> observableFactory,
  final Action1<? super Resource> disposeAction
)

这看起来相当复杂,但仔细看一下就不难理解了。让我们来看一下以下描述:

  • 它的第一个参数是 Func0<Resource> resourceFactory,一个创建 Resource 对象的函数(这里 Resource 是一个任意对象;它不是接口或类,而是类型参数的名称)。我们的工作是实现资源的创建。

  • Func1<? super Resource, ? extends Observable<? extends T>> observableFactory 参数,第二个参数,是一个接收 Resource 对象并返回 Observable 实例的函数。这个函数将使用我们已经通过第一个参数创建的 Resource 对象进行调用。我们可以使用这个资源来创建我们的 Observable 实例。

  • Action1<? super Resource> disposeAction 参数在应该处理 Resource 对象时被调用。它接收了由 resourceFactory 参数创建的 Resource 对象(并用于创建 Observable 实例),我们的工作是处理它。这在取消订阅时被调用。

我们能够创建一个函数,进行 HTTP 请求,而现在不需要传递 HttpAsyncClient 对象。我们有工具可以根据需要创建和处理它。让我们来实现这个函数:

// (1)
public Observable<ObservableHttpResponse> request(String url) {
  Func0<CloseableHttpAsyncClient> resourceFactory = () -> {
    CloseableHttpAsyncClient client = HttpAsyncClients.createDefault(); // (2)
 client.start();
    System.out.println(
      Thread.currentThread().getName() +
      " : Created and started the client."
    );
    return client;
  };
  Func1<HttpAsyncClient, Observable<ObservableHttpResponse>> observableFactory = (client) -> { // (3)
    System.out.println(
      Thread.currentThread().getName() + " : About to create Observable."
    );
    return ObservableHttp.createGet(url, client).toObservable();
  };
  Action1<CloseableHttpAsyncClient> disposeAction = (client) -> {
    try { // (4)
      System.out.println(
        Thread.currentThread().getName() + " : Closing the client."
      );
      client.close();
    }
    catch (IOException e) {}
  };
  return Observable.using( // (5)
 resourceFactory,
 observableFactory,
 disposeAction
 );
}

这个方法并不难理解。让我们来分解一下:

  1. 该方法的签名很简单;它只有一个参数,URL。调用该方法的调用者不需要创建和管理 CloseableHttpAsyncClient 实例的生命周期。它返回一个能够发出 ObservableHttpResponse 响应并完成Observable 实例。getJson() 方法可以使用它将 ObservableHttpResponse 响应转换为表示 JSON 的 Map 实例,而无需传递 client

  2. resourceFactory lambda 很简单;它创建了一个默认的 CloseableHttpAsyncClient 实例并启动它。当被调用时,它将返回一个初始化的 HTTP client,能够请求远程服务器数据。我们输出 client 已准备好用于调试目的。

  3. observableFactory 函数可以访问由 resourceFactory 函数创建的 CloseableHttpAsyncClient 实例,因此它使用它和传递的 URL 来构造最终的 Observable 实例。这是通过 RxJava 的 rxjava-apache-http 模块 API(github.com/ReactiveX/RxApacheHttp)完成的。我们输出我们正在做的事情。

  4. disposeAction 函数接收了用于创建 Observable 实例的 CloseableHttpAsyncClient 对象并对其进行关闭。同样,我们打印一条消息到标准输出,说明我们即将这样做。

  5. 借助 using() 工厂方法,我们返回我们的 HTTP request Observable 实例。这不会触发任何三个 lambda 中的任何一个。订阅返回的 Observable 实例将调用 resourceFactory 函数,然后调用 observableFactory 函数。

这就是我们实现了一个能够管理自己资源的 Observable 实例。让我们看看它是如何使用的:

String url = "https://api.github.com/orgs/ReactiveX/repos";

Observable<ObservableHttpResponse> response = request(url);

System.out.println("Not yet subscribed.");

Observable<String> stringResponse = response
.<String>flatMap(resp -> resp.getContent()
.map(bytes -> new String(bytes, java.nio.charset.StandardCharsets.UTF_8))
.retry(5)

.map(String::trim);

System.out.println("Subscribe 1:");
System.out.println(stringResponse.toBlocking().first());

System.out.println("Subscribe 2:");
System.out.println(stringResponse.toBlocking().first());

我们使用新的 request() 方法来列出 ReactiveX 组织的存储库。我们只需将 URL 传递给它,就会得到一个 Observable 响应。在我们订阅它之前,不会分配任何资源,也不会执行任何请求,所以我们打印出你还没有订阅。

stringResponse 可观察对象包含逻辑并将原始的 ObservableHttpResponse 对象转换为 String。但是,没有分配任何资源,也没有发送请求。

我们使用 BlockingObservable 类的 first() 方法订阅 Observable 实例并等待其结果。我们将响应作为 String 检索并输出它。现在,资源已分配并发出了请求。在获取数据后,BlockingObservable 实例封装的 subscriber 会自动取消订阅,因此使用的资源(HTTP 客户端)被处理掉。我们进行第二次订阅,以查看接下来会发生什么。

让我们来看一下这个程序的输出:

Not yet subscribed.
Subscribe 1:
main : Created and started the client.
main : About to create Observable.
[{"id":7268616,"name":"Rx.rb","full_name":"ReactiveX/Rx.rb",...
Subscribe 2:
I/O dispatcher 1 : Closing the client.
main : Created and started the client.
main : About to create Observable.
I/O dispatcher 5 : Closing the client.
[{"id":7268616,"name":"Rx.rb","full_name":"ReactiveX/Rx.rb",...

因此,当我们订阅网站时,HTTP 客户端和Observable实例是使用我们的工厂 lambda 创建的。创建在当前主线程上执行。发出请求并打印(此处裁剪)。客户端在 IO 线程上被处理,当Observable实例完成执行时,请求被执行。

第二次订阅时,我们从头开始经历相同的过程;我们分配资源,创建Observable实例并处理资源。这是因为using()方法的工作方式——它为每个订阅分配一个资源。我们可以使用不同的技术来重用下一次订阅的相同结果,而不是进行新的请求和分配资源。例如,我们可以为多个订阅者重用CompositeSubscription方法或Subject实例。然而,有一种更简单的方法可以重用下一次订阅的获取响应。

使用 Observable.cache 进行数据缓存

我们可以使用缓存将响应缓存在内存中,然后在下一次订阅时,而不是再次请求远程服务器,使用缓存的数据。

让我们将代码更改为如下所示:

String url = "https://api.github.com/orgs/ReactiveX/repos";
Observable<ObservableHttpResponse> response = request(url);

System.out.println("Not yet subscribed.");
Observable<String> stringResponse = response
.flatMap(resp -> resp.getContent()
.map(bytes -> new String(bytes)))
.retry(5)
.cast(String.class)
.map(String::trim)
.cache();

System.out.println("Subscribe 1:");
System.out.println(stringResponse.toBlocking().first());

System.out.println("Subscribe 2:");
System.out.println(stringResponse.toBlocking().first());

stringResponse链的末尾调用的cache()操作符将为所有后续的subscribers缓存由string表示的响应。因此,这次的输出将是:

Not yet subscribed.
Subscribe 1:
main : Created and started the client.
main : About to create Observable.
[{"id":7268616,"name":"Rx.rb",...
I/O dispatcher 1 : Closing the client.
Subscribe 2:
[{"id":7268616,"name":"Rx.rb",...

现在,我们可以在程序中重用我们的stringResponse Observable实例,而无需进行额外的资源分配和请求。

注意

演示源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter08/ResourceManagement.java找到。

最后,requestJson()方法可以这样实现:

public Observable<Map> requestJson(String url) {
Observable<String> rawResponse = request(url)

....

return Observable.amb(fromCache(url), response);
}

更简单,具有资源自动管理(资源,即 http 客户端会自动创建和销毁),该方法还实现了自己的缓存功能(我们在第五章中实现了它,组合器、条件和错误处理)。

注意

书中开发的所有创建Observable实例的方法都可以在[github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/common/CreateObservable.java 类](https://github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/common/CreateObservable.java 类)中找到。那里还有一个requestJson()方法的文件缓存实现。

有了这个,我们可以扩展 RxJava,创建自己的工厂方法,使Observable实例依赖于任意数据源。

本章的下一部分将展示如何将我们自己的逻辑放入Observable操作符链中。

使用 lift 创建自定义操作符

在学习和使用了许多不同的操作符之后,我们已经准备好编写自己的操作符。Observable类有一个名为lift的操作符。它接收Operator接口的实例。这个接口只是一个空的接口,它扩展了Func1<Subscriber<? super R>, Subscriber<? super T>>接口。这意味着我们甚至可以将 lambda 作为操作符传递。

学习如何使用lift操作符的最佳方法是编写一个示例。让我们创建一个操作符,为发出的每个项目添加一个顺序索引(当然,这也可以在没有专用操作符的情况下完成)。这样,我们将能够生成带有索引的项目。为此,我们需要一个存储项目及其索引的类。让我们创建一个更通用的称为Pair的类:

public class Pair<L, R> {
  final L left;
  final R right;

public Pair(L left, R right) {
    this.left = left;
    this.right = right;
  }

  public L getLeft() {
    return left;
  }

public R getRight() {
    return right;
  }

  @Override
  public String toString() {
    return String.format("%s : %s", this.left, this.right);
  }

// hashCode and equals omitted

}'

这个类的实例是非常简单的不可变对象,包含两个任意对象。在我们的例子中,left字段将是类型为Long的索引,right字段将是发射的项。Pair类,和任何不可变类一样,包含了hashCode()equals()方法的实现。

以下是运算符的代码:

public class Indexed<T> implements Operator<Pair<Long, T>, T> {
  private final long initialIndex;
  public Indexed() {
    this(0L);
  }
  public Indexed(long initial) {
    this. initialIndex = initial;
  }
  @Override
  public Subscriber<? super T> call(Subscriber<? super Pair<Long, T>> s) {
 return new Subscriber<T>(s) {
      private long index = initialIndex;
 @Override
 public void onCompleted() {
 s.onCompleted();
 }
 @Override
 public void onError(Throwable e) {
 s.onError(e);
 }
 @Override
 public void onNext(T t) {
 s.onNext(new Pair<Long, T>(index++, t));
 }
 };
 }
}

Operator接口的call()方法有一个参数,一个Subscriber实例。这个实例将订阅由lift()运算符返回的可观察对象。该方法返回一个新的Subscriber实例,它将订阅调用了lift()运算符的可观察对象。我们可以在其中更改所有通知的数据,这就是我们将编写我们自己运算符逻辑的方式。

Indexed类有一个状态——index。默认情况下,它的初始值是0,但是有一个构造函数可以创建一个具有任意初始值的Indexed实例。我们的运算符将OnErrorOnCompleted通知无修改地委托给订阅者。有趣的方法是onNext()。它通过创建一个Pair实例和index字段的当前值来修改传入的项。之后,index被递增。这样,下一个项将使用递增的index并再次递增它。

现在,我们有了我们的第一个运算符。让我们编写一个单元测试来展示它的行为:

@Test
public void testGeneratesSequentialIndexes() {
  Observable<Pair<Long, String>> observable = Observable
    .just("a", "b", "c", "d", "e")
    .lift(new Indexed<String>());
  List<Pair<Long, String>> expected = Arrays.asList(
    new Pair<Long, String>(0L, "a"),
    new Pair<Long, String>(1L, "b"),
    new Pair<Long, String>(2L, "c"),
    new Pair<Long, String>(3L, "d"),
    new Pair<Long, String>(4L, "e")
  );
  List<Pair<Long, String>> actual = observable
    .toList()
    .toBlocking().
    single();
  assertEquals(expected, actual);
  // Assert that it is the same result for a second subscribtion.
  TestSubscriber<Pair<Long, String>> testSubscriber = new TestSubscriber<Pair<Long, String>>();
  observable.subscribe(testSubscriber);
  testSubscriber.assertReceivedOnNext(expected);
}

测试发射从'a''e'的字母,并使用lift()运算符将我们的Indexed运算符实现插入到可观察链中。我们期望得到一个由从零开始的顺序数字——索引和字母组成的五个Pair实例的列表。我们使用toList().toBlocking().single()技术来检索实际发射项的列表,并断言它们是否等于预期的发射。因为Pair实例有定义了hashCode()equals()方法,我们可以比较Pair实例,所以测试通过了。如果我们第二次订阅Indexed运算符应该从初始索引0开始提供索引。我们使用TestSubscriber实例来做到这一点,并断言字母被索引,从0开始。

注意

Indexed运算符的代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter08/Lift.java找到,以及测试其行为的单元测试可以在github.com/meddle0x53/learning-rxjava/blob/master/src/test/java/com/packtpub/reactive/chapter08/IndexedTest.java找到。

使用lift()运算符和不同的Operator实现,我们可以编写我们自己的运算符,这些运算符作用于发射序列的每个单独项。但在大多数情况下,我们将能够在不创建新运算符的情况下实现我们的逻辑。例如,索引行为可以以许多不同的方式实现,其中一种方式是通过与Observable.range方法合并,就像这样:

Observable<Pair<Long, String>> indexed = Observable.zip(
  Observable.just("a", "b", "c", "d", "e"),
  Observable.range(0, 100),
  (s, i) -> new Pair<Long, String>((long) i, s)
);
subscribePrint(indexed, "Indexed, no lift");

实现新的运算符有许多陷阱,比如链接订阅、支持背压和重用变量。如果可能的话,我们应该尝试组合现有的由经验丰富的 RxJava 贡献者编写的运算符。因此,在某些情况下,一个转换Observable本身的运算符是一个更好的主意,例如,将多个运算符应用于它作为一个。为此,我们可以使用组合运算符compose()

使用 Observable.compose 运算符组合多个运算符

compose()操作符有一个Transformer类型的参数。Transformer接口,就像Operator一样,是一个接口,它扩展了Func1(这种方法隐藏了使用Func1所涉及的类型复杂性)。不同之处在于它扩展了Func1<Observable<T>, Observable<R>>方法,这样它就可以转换一个Observable而不是一个Subscriber。这意味着它不是在observable 发出的每个单独项目上操作,而是直接在源上操作。

我们可以通过一个例子来说明这个操作符和Transformer接口的使用。首先,我们将创建一个Transformer实现:

public class OddFilter<T> implements Transformer<T, T> {
  @Override
  public Observable<T> call(Observable<T> observable) {
    return observable
      .lift(new Indexed<T>(1L))
      .filter(pair -> pair.getLeft() % 2 == 1)
      .map(pair -> pair.getRight());
  }
}

这个实现的思想是根据 observable 发出的顺序来过滤它们的发射。它在整个序列上操作,使用我们的Indexed操作符为每个项目添加一个索引。然后,它过滤具有奇数索引的Pair实例,并从过滤后的Pair实例中检索原始项目。这样,只有在奇数位置上的发射序列成员才会到达订阅者。

让我们再次编写一个单元测试,确保新的OddFilter转换器的行为是正确的:

@Test
public void testFiltersOddOfTheSequence() {
  Observable<String> tested = Observable
    .just("One", "Two", "Three", "Four", "Five", "June", "July")
    .compose(new OddFilter<String>());
  List<String> expected =
    Arrays.asList("One", "Three", "Five", "July");
  List<String> actual = tested
    .toList()
    .toBlocking()
    .single();
  assertEquals(expected, actual);
}

正如你所看到的,我们的OddFilter类的一个实例被传递给compose()操作符,这样,它就被应用到了由range()工厂方法创建的 observable 上。这个 observable 发出了七个字符串。如果OddFilter的实现正确,它应该过滤掉在奇数位置发出的字符串。

注意

OddFilter类的源代码可以在github.com/meddle0x53/learning-rxjava/blob/master/src/main/java/com/packtpub/reactive/chapter08/Compose.java找到。测试它的单元测试可以在github.com/meddle0x53/learning-rxjava/blob/master/src/test/java/com/packtpub/reactive/chapter08/IndexedTest.java中查看/下载。

关于实现自定义操作符的更多信息可以在这里找到:github.com/ReactiveX/RxJava/wiki/Implementing-Your-Own-Operators。如果你在 Groovy 等动态语言中使用 RxJava,你可以扩展Observable类以添加新方法,或者你可以使用 Xtend,这是一种灵活的 Java 方言。参考mnmlst-dvlpr.blogspot.de/2014/07/rxjava-and-xtend.html

总结

创建我们自己的操作符和依赖资源的Observable实例给了我们在围绕Observable类创建逻辑时无限的可能性。我们能够将每个数据源转换成一个Observable实例,并以许多不同的方式转换传入的数据。

我希望这本书涵盖了 RxJava 最有趣和重要的部分。如果我漏掉了重要的内容,github.com/ReactiveX/RxJava/wiki上的文档是网络上最好的之一。特别是在这一部分,可以找到更多阅读材料:github.com/ReactiveX/RxJava/wiki/Additional-Reading

我试图将代码和想法进行结构化,并在各章节中进行小的迭代。第一章和第二章更具有意识形态性;它们向读者介绍了函数式编程和响应式编程的基本思想,第二章试图建立Observable类的起源。第三章为读者提供了创建各种不同Observable实例的方法。第四章和第五章教会我们如何围绕这些Observable实例编写逻辑,第六章将多线程添加到这个逻辑中。第七章涉及读者学会编写的逻辑的单元测试,第八章试图进一步扩展这个逻辑的能力。

希望读者发现这本书有用。不要忘记,RxJava 只是一个工具。重要的是你的知识和思维。

posted @ 2025-09-10 15:06  绝不原创的飞龙  阅读(19)  评论(0)    收藏  举报