RxJava-学习指南-全-
RxJava 学习指南(全)
原文:
zh.annas-archive.org/md5/7bcf42428e8d5d07b326c8c9ca0ccf7b译者:飞龙
前言
响应式编程不仅仅是技术或库规范,它是一种全新的解决问题的心态。它之所以如此有效和革命性,是因为它不是将我们的世界结构化为一系列状态,而是将其视为不断运动的东西。能够快速捕捉运动的复杂性和动态性质(而不是状态),为我们如何用代码表示事物开辟了强大的新可能性。
当我最初学习 Java 和面向对象编程时,我觉得它很有用,但效果并不足够显著。尽管面向对象编程很有用,但我认为它需要与其他技术结合才能真正提高生产力,这就是为什么我一直关注 C#和 Scala。仅仅几年后,Java 8 发布了,我第一次将函数式编程付诸实践。
然而,仍然有些东西缺失。我对一个值通知另一个值其变化,以及一个事件触发另一个事件多米诺效应的想法产生了浓厚的兴趣。难道没有一种方法可以以流畅和函数式的方式对事件进行建模,就像 Java 8 Streams 一样吗?有一天,当我表达了这个想法时,有人向我介绍了响应式编程。我一直在寻找的正是 RxJava 的 Observable,乍一看,它很像 Java 8 Stream。两者看起来和感觉都很相似,但 Observable 不仅仅是推送数据,还推送事件。就在那一刻,我找到了我一直在寻找的东西。
对于我以及许多人来说,学习 RxJava 的挑战在于缺乏文档和资料。我经常只能通过实验、在 Stack Overflow 上提问和在 GitHub 上搜索一些难以理解的问题来积累知识。由于我在工作中使用 RxJava 来解决一些业务问题,我写了多篇博客文章,分享我在并行化和并发等主题上的发现。令我惊讶的是,这些文章的访问量激增。或许这并不令人惊讶,因为这些主题在其他地方很少被详细记录。当 Packt 出版社邀请我写我的第二本书《学习 RxJava》时,尽管工作量很大,我还是毫不犹豫地接受了这个机会。也许,这本书真的可以一劳永逸地解决文档问题。每个基本概念、用例、有用的技巧和“陷阱”都可以变得易于理解,RxJava 将不再被视为“高级主题”。我相信 RxJava 应该对所有技能水平的专业开发者都易于理解,因为它有效地使难题变得简单,简单的问题变得更简单。它可能需要一点抽象的理解,但立即获得的生产力使得这个小小的障碍物值得。
据我所知,这是第一本关于 RxJava 2.0 的出版书籍,它与 RxJava 1.0 有许多重大差异。您现在正在阅读的这本书是我希望拥有的全面、逐步指南。它力求不遗漏任何细节,不提供未经充分解释的代码。我希望它能帮助您快速发现 RxJava 的价值,并在将其应用于所有努力中取得成功。如果您有任何疑问、反馈或评论,欢迎通过 tmnield@outlook.com 联系我。
祝你好运!
托马斯·尼尔
本书涵盖内容
第一章,反应式思维,为您介绍 RxJava。
第二章,Observables 和 Subscribers,讨论了 RxJava 的核心类型,包括 Observable 和 Observer。
第三章,基本操作符,为您提供了对允许您快速表达逻辑并使 RxJava 高效的核心操作符的全面介绍。
第四章,组合 Observables,教您如何以各种方式有效地组合多个 Observable 源。
第五章,多播、重放和缓存,将流合并以防止多个观察者产生冗余工作,以及重放和缓存发射。
第六章,并发和并行化,帮助您发现 RxJava 如何灵活且强大地使您的应用程序实现并发。
第七章,切换、节流、窗口化、和缓冲,阐述了应对快速产生 Observables 而不产生背压的策略。
第八章,Flowables 和背压,利用 Flowable 来利用背压并防止生产者超过消费者的速度。
第九章,转换器和自定义操作符,教您如何重用反应式逻辑并创建自己的 RxJava 操作符。
第十章,测试和调试,利用有效的工具来测试和调试您的 RxJava 代码库。
第十一章,RxJava 在 Android 中的应用,教您如何将 RxJava 知识和 RxAndroid 扩展应用到您的 Android 应用中,以简化开发流程。
第十二章,使用 Kotlin 新特性的 RxJava,利用 Kotlin 的语言特性来启用 RxJava 的表达式模式。
您需要为此书准备的内容
我们将使用 Java 8,因此需要 Oracle 的 JDK 1.8。您需要一个环境来编写和编译 Java 代码(我推荐 IntelliJ IDEA),并且最好有一个构建自动化系统,如 Gradle 或 Maven。本书后面将使用 Android Studio。
本书中的所有内容都应免费使用,无需商业或个人许可。
本书面向的对象
本书面向的是已经掌握了面向对象编程和 Java 核心特性的 Java 程序员。你应该熟悉变量、类型、类、属性、方法、泛型、继承、接口以及静态类/属性/方法。在 Java 标准库中,你应该至少熟悉集合(包括列表、集合和映射)以及对象相等性(hashcode()/equals())。如果这些主题听起来不熟悉,你可能想阅读 Herbert Schildt 的《Java:入门指南》以学习 Java 的基础知识。此外,Joshua Bloch 的《Effective Java(第 2 版)》是一本经典书籍,应该放在每个 Java 开发者的书架上。本书力求使用 Bloch 引用的最佳实践。
您不需要熟悉并发作为先决条件。这个主题将从 RxJava 的角度进行讲解。
术语约定
在本书中,你将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“我们还可以在Observable和Observer之间使用几个操作符来转换每个推送的项目或以某种方式操作它们。”
代码块应如下设置:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> myStrings =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
}
}
任何输出都应如下编写:
Alpha
Beta
Gamma
Delta
Epsilon
新术语和重要词汇将以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,将以如下形式显示:“您还有使用 Maven 的选项,并且可以通过选择 Apache Maven 配置信息在中央仓库中查看相应的配置。”
警告或重要提示将以如下框中的形式出现。
技巧和窍门将以如下形式出现。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中受益的标题。
要向我们发送一般反馈,只需发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍的标题。
如果你在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经成为 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的“支持”选项卡上。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的地方。
-
点击“代码下载”。
下载文件后,请确保您使用最新版本解压缩或提取文件夹。
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learning-RxJava。我们还有其他来自我们丰富图书和视频目录的代码包可供在github.com/PacktPublishing/找到。查看它们吧!
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的“勘误”部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在“勘误”部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您的帮助,保护我们的作者以及我们为您提供有价值内容的能力。
问答
如果您在这本书的任何方面遇到问题,您可以联系我们的questions@packtpub.com,我们将尽力解决问题。
第一章:反应式思维
假设您对 Java 比较熟悉,并且知道如何使用类、接口、方法、属性、变量、静态/非静态作用域和集合。如果您没有进行过并发或多线程编程,那也无所谓。RxJava 使这些高级主题变得更加容易理解。
准备好您喜欢的 Java 开发环境,无论是 Intellij IDEA、Eclipse、NetBeans 还是您选择的任何其他环境。我将使用 Intellij IDEA,尽管这不应该影响本书中的示例。我建议您还应该有一个构建自动化系统,例如 Gradle 或 Maven,我们很快就会介绍这些。
在我们深入探讨 RxJava 之前,我们首先会介绍一些核心主题:
-
Reactive Extensions 和 RxJava 的简要历史
-
反应式思维
-
利用 RxJava
-
设置您的第一个 RxJava 项目
-
构建您的第一个反应式应用程序
-
RxJava 1.0 和 RxJava 2.0 之间的区别
ReactiveX 和 RxJava 的简要历史
作为开发者,我们倾向于训练自己以反直觉的方式思考。用代码来模拟我们的世界从未缺乏挑战。面向对象编程被看作是解决这个问题的银弹,不久前还被视为革命性的想法,而类和对象的核心概念至今仍在影响我们的编码方式。然而,业务和用户需求持续增长,复杂性也在增加。随着 2010 年的临近,很明显,面向对象编程只解决了问题的一部分。
类和对象在表示具有属性和方法的对象方面做得很好,但当它们需要以越来越复杂(并且通常是未计划)的方式相互交互时,它们就会变得混乱。解耦模式和范式出现了,但这导致了大量样板代码的副作用。为了应对这些问题,函数式编程开始复兴,不是为了取代面向对象编程,而是为了补充它并填补这个空白。反应式编程,一种函数式事件驱动编程方法,开始受到特别的关注。
最终出现了几个反应式框架,包括 Akka 和 Sodium。但在微软,一位名叫 Erik Meijer 的计算机科学家创建了一个名为 Reactive Extensions 的反应式编程框架。在短短几年内,Reactive Extensions(也称为 ReactiveX 或 Rx)被移植到包括 JavaScript、Python、C++、Swift 和 Java 在内的多种语言和平台上。ReactiveX 迅速成为跨语言标准,将反应式编程带入行业。
RxJava,Java 的 ReactiveX 端口,主要由 Netflix 的 Ben Christensen 和 David Karnok 创建。RxJava 1.0 于 2014 年 11 月发布,随后在 2016 年 11 月发布了 RxJava 2.0。RxJava 是其他 ReactiveX JVM 端口的支柱,例如RxScala、RxKotlin和RxGroovy。它已成为 Android 开发的核心技术,并且也进入了 Java 后端开发领域。许多 RxJava 适配库,如RxAndroid (github.com/ReactiveX/RxAndroid)、RxJava-JDBC (github.com/davidmoten/rxjava-jdbc)、RxNetty (github.com/ReactiveX/RxNetty)和RxJavaFX (github.com/ReactiveX/RxJavaFX),将几个 Java 框架适配为响应式,并能够与 RxJava 无缝协作。
所有这些都表明,RxJava 不仅仅是一个库。它是更大的 ReactiveX 生态系统的一部分,代表了整个编程方法。ReactiveX 的基本思想是事件是数据,数据是事件。这是一个强大的概念,我们将在本章后面进行探讨,但首先,让我们退后一步,用响应式视角来看待这个世界。
响应式思考
暂时放下你对 Java(以及一般编程)的所有已知知识,让我们对我们的世界做一些观察。这些可能听起来像是显而易见的陈述,但作为开发者,我们很容易忽略它们。请注意,一切都在运动。交通、天气、人们、对话、金融交易等等都在移动。技术上,甚至像石头这样静止的东西也因为地球的旋转和轨道而处于运动状态。当你考虑一切都可以被建模为运动的可能性时,你可能会发现这作为开发者来说有点令人不知所措。
另一个值得注意的观察是,这些不同的事件是同时发生的。多个活动在同一时间进行。有时,它们独立行动,但有时,它们可以在某个点上汇聚以进行交互。例如,一辆车在跑步者没有受到影响的情况下行驶。它们是两个独立的事件流。然而,它们可能在某个点上汇聚,当车遇到跑步者时,车会停下来。
如果我们的世界就是这样运作的,为什么我们不以这种方式来建模我们的代码呢?为什么我们不把代码建模为同时发生的多个并发的事件或数据流呢?开发者花费更多时间管理对象的状态,并以命令式和顺序的方式进行操作,这种情况并不少见。你可能将你的代码结构化为执行过程 1、过程 2,然后是过程 3,这依赖于过程 1 和过程 2。为什么不同时启动过程 1 和过程 2,然后这两个事件的完成立即启动过程 3 呢?当然,你可以使用回调和 Java 并发工具,但 RxJava 使这变得更加容易和安全地表达。
让我们做一个最后的观察。一本书或音乐 CD 是静态的。一本书是一系列不变的文字,CD 是一系列曲目。它们没有动态性。然而,当我们读书时,我们是逐个阅读每个单词。这些单词实际上被置于运动中,作为被我们眼睛消费的流。音乐 CD 的曲目也是如此,每个曲目被置于运动中,作为声波,你的耳朵消费每个曲目。静态物品实际上也可以被置于运动中。这是一个抽象但强大的想法,因为我们把每个静态物品变成了一系列事件。当我们通过将数据和事件同等对待来使数据与事件处于同一水平时,我们就释放了函数式编程的力量,并解锁了你以前可能认为不切实际的能力。反应式编程背后的基本思想是事件是数据,数据是事件。这可能看起来很抽象,但当你考虑我们的现实世界例子时,实际上并不需要很长时间就能理解。跑步者和汽车都有属性和状态,但它们也在运动中。当它们被消费时,书和 CD 被置于运动中。将事件和数据合并为一个整体,使得代码感觉有机且能代表我们正在建模的世界。
我为什么要学习 RxJava?
ReactiveX 和 RxJava 为程序员每天面临的许多问题画了一幅广泛的画,允许你表达业务逻辑,并减少编写代码的时间。你是否曾经为并发、事件处理、过时的数据状态和异常恢复而挣扎过?关于使你的代码更具可维护性、可重用性和可扩展性,以便它能跟上你的业务?称反应式编程为这些问题的银弹可能有些自以为是,但它确实是在解决这些问题方面的一个进步飞跃。
用户对使应用程序实时和响应的需求也在不断增长。反应式编程允许你快速分析和处理实时数据源,如 Twitter 流或股价。它还可以取消和重定向工作,与并发性一起扩展,并处理快速发射的数据。将事件和数据作为可以混合、合并、过滤、拆分和转换的流来组合,开辟了极其有效的代码组合和演变方式。
总结来说,反应式编程使得许多困难任务变得简单,使你能够以你之前可能认为不切实际的方式增加价值。如果你有一个编写为反应式的进程,并且发现你需要将其中一部分运行在不同的线程上,你可以在几秒钟内实现这个更改。如果你发现网络连接问题间歇性地使你的应用程序崩溃,你可以优雅地使用等待并重试的反应式恢复策略。如果你需要在进程的中间注入一个操作,这就像插入一个新的操作符一样简单。反应式编程被分解为模块化的链链接,可以添加或删除,这有助于快速克服上述所有问题。本质上,RxJava 允许应用程序在保持生产稳定性同时变得战术性和可演化的。
我们在这本书中将学到什么?
如前所述,RxJava 是 Java 的 ReactiveX 端口。在这本书中,我们将主要关注 RxJava 2.0,但我将指出 RxJava 1.0 中的显著差异。我们将优先学习如何以反应式的方式思考并利用 RxJava 的实用功能。从高层次的理解开始,我们将逐渐深入了解 RxJava 的工作原理。在这个过程中,我们将学习关于反应式模式和解决程序员遇到的常见问题的技巧。
在第二章,可观察者和订阅者,第三章,基本操作符和第四章,合并可观察者中,我们将通过Observable、Observer和Operator来介绍 Rx 的核心概念。这三个核心实体构成了 RxJava 应用程序。你将立即开始编写反应式程序,并拥有一个坚实的基础来构建整本书。
第五章,多播、重放和缓存以及第六章,并发和并行化将探讨 RxJava 的更多细微之处以及如何有效地利用并发。
在第七章,切换、节流、窗口和缓冲以及第八章,可流动性和背压中,我们将学习如何应对产生数据/事件的速度超过其消费速度的反应式流的不同方法。
最后,第九章 转换器和自定义操作符,第十章 测试和调试,第十一章 RxJava 在 Android 上的应用,以及第十二章 使用 Kotlin 新特性与 RxJava 结合 将涉及一些(但很重要)的杂项主题,包括自定义操作符以及如何使用测试框架、Android 和 Kotlin 语言与 RxJava 结合。
设置
目前存在两个共存的 RxJava 版本:1.0 和 2.0。我们将在稍后讨论一些主要差异,并讨论你应该使用哪个版本。
RxJava 2.0 是一个相对轻量级的库,其大小仅为 2 兆字节(MBs)。这使得它在需要低依赖开销的 Android 和其他项目中非常实用。RxJava 2.0 只有一个依赖项,称为 Reactive Streams(www.reactive-streams.org/),这是一个核心库(由 RxJava 的创建者制作),为异步流实现设定了标准,其中之一就是 RxJava 2.0。
它可以用于 RxJava 之外的库,并且是 Java 平台上反应式编程标准化的关键努力。请注意,RxJava 1.0 没有任何依赖项,包括 Reactive Streams,这是在 1.0 之后实现的。
如果你是从零开始一个项目,尝试使用 RxJava 2.0。这是我们将在本书中介绍的这个版本,但我会指出 1.0 版本中的重大差异。虽然由于无数项目在使用它,RxJava 1.0 将会得到一段时间的支持,但创新可能只会继续在 RxJava 2.0 中进行。RxJava 1.0 将只获得维护和错误修复。
RxJava 1.0 和 2.0 都运行在 Java 1.6+ 上。在这本书中,我们将使用 Java 8,并建议你使用至少 Java 8,这样你就可以直接使用 lambda 表达式。对于 Android,稍后我们将讨论如何利用早期 Java 版本的 lambda 表达式。但考虑到 Android Nougat 使用 Java 8,而 Java 8 自 2014 年以来就已经发布,希望你不需要进行任何工作来利用 lambda 表达式。
导航中央仓库
要将 RxJava 作为依赖项引入,你有几种选择。最好的开始方法是访问中央仓库(搜索 search.maven.org/)并搜索 rxjav。你应该在搜索结果顶部看到 RxJava 2.0 和 RxJava 1.0 作为单独的仓库,如下面的截图所示:

在中央仓库中搜索 RxJava(RxJava 2.0 和 1.0 被突出显示)
在撰写本文时,RxJava 2.0.2 是 RxJava 2.0 的最新版本,RxJava 1.2.3 是 RxJava 1.0 的最新版本。您可以通过点击下载列最右侧的 JAR 链接下载任何一个的最新 JAR 文件。然后,您可以配置您的项目使用该 JAR 文件。
然而,您可能还想考虑使用 Gradle 或 Maven 自动将这些库导入到您的项目中。这样,您可以轻松地共享和存储您的代码项目(通过 GIT 或其他版本控制系统),而无需每次都手动下载和配置 RxJava。要查看 Maven、Gradle 以及其他几个构建自动化系统的最新配置,请点击任一存储库的版本号,如下面的截图所示:

点击“最新版本”列下的版本号,可以查看 Maven、Gradle 以及其他主要构建自动化系统的配置。
使用 Gradle
可用的自动化构建系统有很多,但最主流的两个选项是 Gradle 和 Maven。Gradle 可以说是 Maven 的继承者,并且是 Android 开发的首选构建自动化解决方案。如果您不熟悉 Gradle 并且想学习如何使用它,请查看 Gradle 入门指南 (gradle.org/getting-started-gradle-java/)。
此外,还有一些不错的书籍涵盖了不同深度的 Gradle,您可以在 gradle.org/books/ 找到它们。以下截图显示了中央仓库页面,展示了如何为 Gradle 设置 RxJava 2.0.2:

您可以在 Gradle 脚本中找到最新的 Gradle 配置代码并将其复制进去
在您的 build.gradle 脚本中,请确保您已声明 mavenCentral() 作为您的仓库之一。输入或粘贴以下依赖项行 compile 'io.reactivex.rxjava2:rxjava:x.y.z',其中 x.y.z 是您想要使用的版本号,如下面的代码片段所示:
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile 'io.reactivex.rxjava2:rxjava:x.y.z'
}
构建 Gradle 项目,您应该就可以使用了!然后,您将在项目中可以使用 RxJava 及其类型。
使用 Maven
您还有选择使用 Maven 的选项,您可以通过选择以下截图所示的 Apache Maven 配置信息在中央仓库中查看相应的配置:

选择并复制 Apache Maven 配置
然后,您可以复制并粘贴包含 RxJava 配置的 <dependency> 块,并将其粘贴到您的 pom.xml 文件中的 <dependencies> 块内。重新构建您的项目,现在您应该已经将 RxJava 设置为依赖项。x.y.z 版本号对应于您想要使用的 RxJava 版本:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.nield</groupId>
<artifactId>mavenrxtest</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
<version>x.y.z</version>
</dependency>
</dependencies>
</project>
快速了解 RxJava
在我们深入探讨 RxJava 的响应式世界之前,这里有一个快速介绍,让你先湿一下脚。在 ReactiveX 中,你将工作的核心类型是Observable。我们将在本书的其余部分学习更多关于Observable的内容。但基本上,Observable推送事物。给定的Observable<T>通过一系列操作符推送类型为T的事物,直到到达一个消费这些项的Observer。
例如,在你的项目中创建一个新的Launcher.java文件,并放入以下代码:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> myStrings =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
}
}
在我们的main()方法中,我们有一个将推送五个字符串对象的Observable<String>。Observable可以从几乎任何来源推送数据或事件,无论是数据库查询还是实时 Twitter 流。在这种情况下,我们使用Observable.just()快速创建一个Observable,它将发射一组固定项。
在 RxJava 2.0 中,你将使用的多数类型都包含在io.reactivex包中。在 RxJava 1.0 中,类型包含在rx包中。
然而,运行这个main()方法除了声明Observable<String>之外不会做任何事情。为了使这个Observable实际上推送这五个字符串(称为排放),我们需要一个Observer来订阅它并接收这些项。我们可以通过传递一个 lambda 表达式来快速创建和连接一个Observer,该 lambda 表达式指定对每个接收到的字符串要执行的操作:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> myStrings =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
myStrings.subscribe(s -> System.out.println(s));
}
}
当我们运行这段代码时,我们应该得到以下输出:
Alpha
Beta
Gamma
Delta
Epsilon
发生在这里的情况是,我们的Observable<String>逐个将字符串对象推送到我们的Observer,我们使用 lambda 表达式s -> System.out.println(s)来简写它。我们通过参数s(我随意命名的)传递每个字符串,并指示它打印每个字符串。Lambda 表达式本质上是一些小函数,允许我们快速传递对每个传入项要采取的操作的指令。箭头->左侧是参数(在这种情况下是一个我们命名为s的字符串),右侧是操作(即System.out.println(s))。
如果你对 lambda 表达式不熟悉,请参阅附录,了解更多关于它们的工作原理。如果你想额外花时间理解 lambda 表达式,我强烈推荐你至少阅读理查德·沃伯顿的《Java 8 Lambdas》(O'Reilly)的前几章(shop.oreilly.com/product/0636920030713.do)。Lambda 表达式是现代编程中的一个关键主题,自从 Java 8 引入以来,对 Java 开发者来说尤其相关。在这本书中,我们将不断使用 lambda 表达式,所以请确保花些时间熟悉它们。
我们也可以在Observable和Observer之间使用几个运算符来转换每个推送的项目或以某种方式操作它们。每个运算符都会返回一个新的Observable,它是从上一个Observable派生出来的,但反映了这种转换。例如,我们可以使用map()将每个字符串发射转换为它的length(),然后每个长度整数将被推送到Observer,如下面的代码片段所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> myStrings =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
myStrings.map(s -> s.length()).subscribe(s ->
System.out.println(s));
}
}
当我们运行这段代码时,我们应该得到以下输出:
5
4
5
5
7
如果你曾经使用过 Java 8 Streams 或 Kotlin 序列,你可能想知道Observable有什么不同。关键的区别是Observable推送项目,而 Streams 和序列则拉取项目。这看起来可能很微妙,但基于推送的迭代的影响远大于基于拉取的迭代。正如我们之前看到的,你可以推送不仅数据,还可以事件。例如,Observable.interval()将在每个指定的时间间隔推送一个连续的Long,如下面的代码片段所示。这个Long发射不仅是数据,也是一个事件!让我们看看:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> secondIntervals =
Observable.interval(1, TimeUnit.SECONDS);
secondIntervals.subscribe(s -> System.out.println(s));
/* Hold main thread for 5 seconds
so Observable above has chance to fire */
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当我们运行这段代码时,我们应该得到以下输出:
0
1
2
3
4
当你运行前面的代码时,你会看到每秒都会触发一个连续的发射。这个应用程序将在大约五秒后退出,你可能会看到发射0到4,每个发射之间只隔了一秒钟。这个简单的想法,即数据是随时间的一系列事件,将为我们解决编程问题提供新的可能性。
顺便说一下,我们将在稍后更深入地探讨并发,但我们必须创建一个sleep()方法,因为当订阅时,这个Observable会在计算线程上触发发射。用于启动我们应用程序的主线程不会等待这个Observable,因为它在计算线程上触发,而不是主线程。因此,我们使用sleep()暂停主线程 5000 毫秒,然后允许它到达main()方法的末尾(这将导致应用程序终止)。这给了Observable.interval()一个在应用程序退出前五秒内触发发射的机会。
在整本书中,我们将揭示许多关于Observable及其为我们处理的强大抽象的奥秘。如果你到目前为止已经从概念上理解了这里发生的事情,恭喜你!你已经开始熟悉响应式代码的工作方式。再次强调,发射是一次性推送到Observer的。发射代表数据和事件,这些事件可以在时间上发射。当然,除了map()之外,RxJava 还有数百个运算符,我们将在本书中学习关键的一些。了解在特定情况下使用哪些运算符以及如何组合它们是掌握 RxJava 的关键。在下一章中,我们将更全面地介绍Observable和Observer。我们还将进一步揭开Observable中代表的事件和数据。
RxJava 1.0 与 RxJava 2.0 对比 - 我该使用哪一个?
如前所述,如果你能使用 RxJava 2.0,我们鼓励你使用它。它将继续增长并接收新功能,而 RxJava 1.0 将仅用于修复错误。然而,还有其他考虑因素可能会让你选择使用 RxJava 1.0。
如果你继承了一个已经使用 RxJava 1.0 的项目,你可能会继续使用它,直到重构到 2.0 成为可行。你也可以查看 David Akarnokd 的 RxJava2Interop 项目 (github.com/akarnokd/RxJava2Interop),该项目可以将 RxJava 1.0 的类型转换为 RxJava 2.0,反之亦然。在你完成这本书之后,你可以考虑使用这个库来利用 RxJava 2.0,即使你有 RxJava 1.0 的遗留代码。
在 RxJava 中,有几个库可以将多个 Java API 转换为响应式,并无缝地集成到 RxJava 中。仅举几个例子,这些库包括 RxJava-JDBC、RxAndroid、RxJava-Extras、RxNetty 和 RxJavaFX。在撰写本文时,只有 RxAndroid 和 RxJavaFX 已经完全移植到 RxJava 2.0(尽管许多其他库也在跟进)。在你阅读本文时,所有主要的 RxJava 扩展库有望被移植到 RxJava 2.0。
你还可能希望优先考虑 RxJava 2.0,因为它建立在从 RxJava 1.0 获得的许多经验和智慧之上。它具有更好的性能、更简单的 API、更清晰的背压处理方法,以及在你编写自定义操作符时提供更多安全性。
何时使用 RxJava
响应式编程新手的常见问题是,在什么情况下需要采取响应式方法?我们是否总是想使用 RxJava?作为一个已经生活在响应式编程中的人,我了解到这个问题有两个答案:
第一个答案是当你刚开始的时候:是的!你总是希望采取响应式的方法。真正成为响应式编程大师的唯一方法是从头开始构建响应式应用程序。将一切视为 Observable,并始终以数据和事件流来建模你的程序。当你这样做时,你将利用响应式编程提供的所有功能,并看到你应用程序的质量显著提高。
第二个答案是,当你对 RxJava 有经验时,你会发现一些情况下 RxJava 可能并不适用。偶尔会有时候,响应式方法可能不是最佳选择,但通常,这个例外只适用于你代码的一部分。你的整个项目本身应该是响应式的。可能会有一些部分不是响应式的,而且有很好的理由。这些例外只会让有经验的 Rx 老兵注意到,返回 List<String> 可能比返回 Observable<String> 更好。
对于 Rx 新手来说,不必担心何时应该使用响应式编程,何时不应该。随着时间的推移,他们将会开始看到那些使 Rx 优势边缘化的案例,而这只有通过经验才能获得。
因此,现在不要妥协。全面拥抱响应式编程!
摘要
在本章中,我们学习了如何以响应式的方式看待世界。作为一个开发者,你可能需要从传统的命令式思维模式中重新训练自己,并发展出响应式思维。特别是如果你长时间从事命令式和面向对象的编程,这可能会是一项挑战。但回报将是显著的,因为你的应用程序将变得更加易于维护、可扩展和可进化。你也将拥有更快的周转时间和更易读的代码。
我们还介绍了如何使用 Gradle 或 Maven 配置 RxJava 项目,以及应该根据什么决策来选择 RxJava 2.0 而不是 RxJava 1.0。我们还简要介绍了响应式代码以及 Observable 如何通过基于推送的迭代工作。
当你完成这本书的时候,你可能会希望发现响应式编程直观且易于理解。我希望你发现 RxJava 不仅使你更高效,还帮助你承担之前犹豫不决的任务。那么,让我们开始吧!
第二章:Observable和订阅者
我们已经在第一章《反应式思考》中窥见了Observable及其工作原理。你可能对它如何精确运作以及它有哪些实际应用有很多疑问。本章将为理解Observable的工作原理及其与Observer的关键关系奠定基础。我们还将介绍几种创建Observable的方法,并通过介绍一些操作符使其变得有用。为了使本书的其余部分流畅,我们还将直接覆盖所有关键细节,以建立一个坚实的基础,避免你在以后遇到惊喜。
本章我们将涵盖以下内容:
-
Observable -
Observer -
其他
Observable工厂 -
Single、Completable和Maybe -
Disposable
Observable
如在第一章《反应式思考》中所述,Observable是一个基于推送的可组合迭代器。对于给定的Observable<T>,它通过一系列操作符推送类型为T的项目(称为发射),直到最终到达一个最终的Observer,它消费这些项目。我们将介绍几种创建Observable的方法,但首先,让我们深入了解Observable是如何通过其onNext()、onCompleted()和onError()调用工作的。
Observable的工作原理
在我们做任何事情之前,我们需要研究Observable如何顺序地将项目传递到链中的Observer。在最高层次上,Observable通过传递三种类型的事件来工作:
-
onNext():这个方法将每个项目逐个从源Observable传递到Observer。 -
onComplete():这个方法将完成事件传递到Observer,表明将不再发生onNext()调用。 -
onError():这个方法将错误传递到链中的Observer,其中Observer通常定义如何处理它。除非使用retry()操作符来拦截错误,否则Observable链通常终止,不再发生更多的发射。
这三个事件是Observer类型中的抽象方法,我们将在稍后介绍一些实现。现在,我们将实用主义地关注它们在日常使用中的工作方式。
在 RxJava 1.0 中,onComplete()事件实际上被称作onCompleted()。
使用 Observable.create()
让我们从使用Observable.create()创建一个源Observable开始。相对而言,源Observable是一个发射起源于此的Observable,并且是我们Observable链的起点。
Observable.create() 工厂允许我们通过提供一个接收 Observable 发射器的 lambda 来创建一个 Observable。我们可以调用 Observable 发射器的 onNext() 方法将发射(一次一个)向上传递到链中,以及调用 onComplete() 来表示完成并传达将没有更多项目。这些 onNext() 调用会将这些项目向上传递到链中的 Observer,它将打印每个项目,如下面的代码片段所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source = Observable.create(emitter -> {
emitter.onNext("Alpha");
emitter.onNext("Beta");
emitter.onNext("Gamma");
emitter.onNext("Delta");
emitter.onNext("Epsilon");
emitter.onComplete();
});
source.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Gamma
RECEIVED: Delta
RECEIVED: Epsilon
在 RxJava 1.0 中,请确保使用 Observable.fromEmitter() 而不是 Observable.create()。在 RxJava 1.0 中,后者是完全不同的东西,仅适用于高级 RxJava 用户。
onNext() 方法是将每个项目(从 Alpha 开始)传递给链中的下一个步骤的一种方式。在这个例子中,下一个步骤是观察者,它使用 s -> System.out.println("RECEIVED: " + s) lambda 打印项目。这个 lambda 在 Observer 的 onNext() 调用中被调用,我们稍后会更详细地了解 Observer。
注意,Observable 协议 (reactivex.io/documentation/contract.html) 规定发射必须按顺序逐个传递。Observable 不能并发或并行地传递发射。这看起来可能是一个限制,但实际上它确实简化了程序,并使 Rx 更容易推理。我们将在第六章 并发和并行化中学习一些强大的技巧,以有效地利用并发和并行化,而不会破坏 Observable 协议。
onComplete() 方法用于向上传递到 Observer,表示没有更多项目到来。实际上,观察者可以是无限的,如果这种情况发生,onComplete() 事件将永远不会被调用。技术上,源可以停止发出 onNext() 调用并永远不调用 onComplete()。但这可能是不良的设计,因为如果源不再计划发送发射。
虽然这个特定的例子不太可能抛出错误,但我们可以在 Observable.create() 块内部捕获可能发生的错误,并通过 onError() 发出它们。这样,错误就可以被推送到链的上方,并由 Observer 处理。我们设置的特定 Observer 并不处理异常,但你可以像下面这样做到:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source = Observable.create(emitter -> {
try {
emitter.onNext("Alpha");
emitter.onNext("Beta");
emitter.onNext("Gamma");
emitter.onNext("Delta");
emitter.onNext("Epsilon");
emitter.onComplete();
} catch (Throwable e) {
emitter.onError(e);
}
});
source.subscribe(s -> System.out.println("RECEIVED: " + s), Throwable::printStackTrace);
}
}
注意,onNext()、onComplete() 和 onError() 并不一定直接推送到最终的观察者。它们也可以推送到作为链中下一个步骤的操作符。在下面的代码中,我们使用 map() 和 filter() 操作符派生新的观察者,这些操作符将在源 Observable 和最终 Observer 打印项目之间起作用:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source = Observable.create(emitter -> {
try {
emitter.onNext("Alpha");
emitter.onNext("Beta");
emitter.onNext("Gamma");
emitter.onNext("Delta");
emitter.onNext("Epsilon");
emitter.onComplete();
} catch (Throwable e) {
emitter.onError(e);
}
});
Observable<Integer> lengths = source.map(String::length);
Observable<Integer> filtered = lengths.filter(i -> i >= 5);
filtered.subscribe(s -> System.out.println("RECEIVED: " +
s));
}
}
这是运行代码后的输出:
RECEIVED: 5
RECEIVED: 5
RECEIVED: 5
RECEIVED: 7
在源 Observable 和 Observer 之间使用 map() 和 filter() 操作符时,onNext() 将每个项目交给 map() 操作符。内部,它将充当中间 Observer,并将每个字符串转换为它的 length()。这将反过来调用 filter() 上的 onNext() 来传递那个整数,lambda 条件 i -> i >= 5 将抑制长度不足五个字符的发射。最后,filter() 操作符将调用 onNext() 将每个项目交给最终的 Observer,在那里它们将被打印出来。
重要的是要注意,map() 操作符将产生一个新的 Observable<Integer>,它是从原始的 Observable<String> 派生出来的。filter() 也会返回一个 Observable<Integer>,但会忽略不符合标准的事件。由于像 map() 和 filter() 这样的操作符产生新的 Observables(它们内部使用 Observer 实现来接收事件),我们可以通过下一个操作符链式连接所有返回的 Observables,而不是将每个 Observables 无需保存到中间变量中:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source = Observable.create(emitter -> {
try {
emitter.onNext("Alpha");
emitter.onNext("Beta");
emitter.onNext("Gamma");
emitter.onNext("Delta");
emitter.onNext("Epsilon");
emitter.onComplete();
} catch (Throwable e) {
emitter.onError(e);
}
});
source.map(String::length)
.filter(i -> i >= 5)
.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
输出如下:
RECEIVED: 5
RECEIVED: 5
RECEIVED: 5
RECEIVED: 7
以这种方式链式连接操作符在响应式编程中很常见(并且被鼓励)。它有一个很好的特性,就像一本书一样,可以从左到右、从上到下阅读,这有助于维护性和可读性。
在 RxJava 2.0 中,Observables 不再支持发出 null 值。如果你创建了一个尝试发出 null 值的 Observable,你将立即得到一个非 null 异常。如果你需要发出 null,考虑将其包装在 Java 8 或 Google Guava Optional 中。
使用 Observable.just()
在我们更详细地查看 subscribe() 方法之前,请注意,你可能不太经常需要使用 Observable.create()。它有助于连接某些非响应式的源,我们将在本章后面的几个地方看到这一点。但通常,我们使用精简的工厂来为常见的源创建 Observables。
在我们之前的 Observable.create() 示例中,我们可以使用 Observable.just() 来完成这个任务。我们可以传递最多 10 个我们想要发出的项目。它将为每个项目调用 onNext() 调用,然后在它们全部被推入后调用 onComplete():
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.map(String::length).filter(i -> i >= 5)
.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
我们还可以使用 Observable.fromIterable() 来从任何 Iterable 类型(如 List)发出项目。它也会为每个元素调用 onNext(),然后在迭代完成后调用 onComplete()。你可能会经常使用这个工厂,因为 Java 中的 Iterables 很常见,并且可以很容易地使其响应式:
import io.reactivex.Observable;
import java.util.Arrays;
import java.util.List;
public class Launcher {
public static void main(String[] args) {
List<String> items =
Arrays.asList("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
Observable<String> source = Observable.fromIterable(items);
source.map(String::length).filter(i -> i >= 5)
.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
我们将在本章后面探索其他创建 Observables 的工厂,但到目前为止,让我们先放一放,更多地了解 Observers。
Observer 接口
onNext()、onComplete()和onError()方法实际上定义了观察者类型,这是一个在 RxJava 中实现以通信这些事件的抽象接口。这是 RxJava 中显示在代码片段中的观察者定义。现在不必担心onSubscribe(),因为我们将在本章末尾讨论它。只需关注其他三个方法:
package io.reactivex;
import io.reactivex.disposables.Disposable;
public interface Observer<T> {
void onSubscribe(Disposable d);
void onNext(T value);
void onError(Throwable e);
void onComplete();
}
观察者和源Observable是相对的。在一个上下文中,源Observable是Observable链开始的地方,也是发射的起源。在我们的前例中,可以说我们从Observable.create()方法或Observable.just()返回的Observable是源Observable。但对filter()操作符来说,从map()操作符返回的Observable是源。它不知道发射从哪里起源,它只知道它立即从上游的操作符接收发射,这些发射来自map()。
相反,每个由操作符返回的Observable在内部都是一个观察者,它接收、转换并将发射传递给下一个Observer。它不知道下一个Observer是另一个操作符还是链尾的最终Observer。当我们谈论Observer时,我们通常指的是Observable链尾的最终Observer,它消费这些发射。但每个操作符,如map()和filter(),也内部实现了Observer。
我们将在第九章变换器和自定义操作符中详细了解操作符是如何构建的。现在,我们将专注于使用Observer来调用subscribe()方法。
在 RxJava 1.0 中,订阅者本质上成为了 RxJava 2.0 中的观察者。在 RxJava 1.0 中有一个定义了三个事件方法的Observer类型,但订阅者是传递给subscribe()方法的,并且实现了Observer。在 RxJava 2.0 中,订阅者仅在讨论Flowables时存在,我们将在第八章 Flowables 和背压中讨论。
实现并订阅一个观察者
当你在Observable上调用subscribe()方法时,使用一个Observer通过实现其方法来消费这三个事件。我们不再像之前那样指定 lambda 参数,我们可以实现一个Observer并将其实例传递给subscribe()方法。现在不必担心onSubscribe(),只需将其实现留空,直到我们在本章末尾讨论它:
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
Observer<Integer> myObserver = new Observer<Integer>() {
@Override
public void onSubscribe(Disposable d) {
//do nothing with Disposable, disregard for now
}
@Override
public void onNext(Integer value) {
System.out.println("RECEIVED: " + value);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done!");
}
};
source.map(String::length).filter(i -> i >= 5)
.subscribe(myObserver);
}
}
输出如下:
RECEIVED: 5
RECEIVED: 5
RECEIVED: 5
RECEIVED: 7
Done!
我们快速创建一个Observer<Integer>作为我们的Observer,它将接收整数长度的发射。我们的Observer在Observable链的末尾接收发射,并作为消耗发射的终点。通过消耗,这意味着它们到达过程的末尾,被写入数据库、文本文件、服务器响应、在 UI 中显示,或者(在这种情况下)只是打印到控制台。
为了更详细地解释这个示例,我们从源头的字符串发射开始。我们提前声明我们的Observer,并在Observable链的末尾将其传递给subscribe()方法。请注意,每个字符串都被转换为其长度。onNext()方法接收每个整数长度的发射,并使用System.out.println("RECEIVED: " + value)打印出来。运行这个简单的过程不会出现任何错误,但如果在Observable链的任何地方发生了错误,它将被推送到Observer上的onError()实现,在那里将打印出Throwable的堆栈跟踪。最后,当源没有更多的发射(在推送"Epsilon"之后),它将调用链中的onComplete()直到 Observer,在那里它的onComplete()方法将被调用,并在控制台打印Done!。
使用 lambda 的简写 Observer
实现Observer有点冗长和繁琐。幸运的是,subscribe()方法被重载以接受我们的三个事件的 lambda 参数。这可能是我们大多数情况下想要使用的,我们可以指定三个用逗号分隔的 lambda 参数:onNext lambda、onError lambda 和onComplete lambda。对于我们之前的示例,我们可以使用这三个 lambda 合并我们的三个方法实现:
Consumer<Integer> onNext = i -> System.out.println("RECEIVED: " + i);
Action onComplete = () -> System.out.println("Done!");
Consumer<Throwable> onError = Throwable::printStackTrace;
我们可以将这三个 lambda 作为参数传递给subscribe()方法,它将使用它们为我们实现一个Observer。这要简洁得多,需要的样板代码也少得多:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.map(String::length).filter(i -> i >= 5)
.subscribe(i -> System.out.println("RECEIVED: " + i),
Throwable::printStackTrace,
() -> System.out.println("Done!"));
}
}
输出如下:
RECEIVED: 5
RECEIVED: 5
RECEIVED: 5
RECEIVED: 7
Done!
注意,subscribe()还有其他重载。您可以省略onComplete(),只实现onNext()和onError()。这将不再为onComplete()执行任何操作,但可能存在不需要一个的情况:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.map(String::length).filter(i -> i >= 5)
.subscribe(i -> System.out.println("RECEIVED: " + i),
Throwable::printStackTrace);
}
}
输出如下:
RECEIVED: 5
RECEIVED: 5
RECEIVED: 5
RECEIVED: 7
如您在之前的示例中看到的,您甚至可以省略onError,只需指定onNext:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.map(String::length).filter(i -> i >= 5)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
然而,在生产中不实现onError()是您想要避免做的事情。在Observable链的任何地方发生的错误都将传播到onError()进行处理,然后没有更多的发射终止Observable。如果您没有为onError指定操作,错误将不会被处理。
如果发生错误,您可以使用retry()运算符尝试恢复并重新订阅一个Observable。我们将在下一章中介绍如何做到这一点。
重要的是要注意,大多数的 subscribe() 重载变体(包括我们刚刚提到的简写 lambda 变体)返回一个 Disposable,我们没有对其进行任何操作。disposables 允许我们断开 Observable 和 Observer 的连接,以便提前终止发射,这对于无限或长时间运行的观察量至关重要。我们将在本章末尾介绍 disposables。
冷观察量与热观察量
根据 Observable 的实现方式,Observable 和 Observer 之间的关系存在细微的行为差异。需要了解的一个重要特征是冷观察量与热观察量,它定义了当存在多个观察者时观察量的行为。首先,我们将介绍冷观察量。
冷观察量
冷观察量非常像一张可以被每位听众重放的音乐 CD,所以每个人都可以在任何时候听到所有曲目。同样,冷观察量将重新播放发射给每个 Observer,确保所有观察者都能获得所有数据。大多数数据驱动的观察量都是冷的,这包括 Observable.just() 和 Observable.fromIterable() 工厂。
在下面的例子中,我们有两个观察者订阅了一个 Observable。Observable 将首先将所有发射传递给第一个 Observer 并调用 onComplete()。然后,它将再次将所有发射传递给第二个 Observer 并调用 onComplete()。他们各自通过获取两个单独的流接收相同的数据集,这是冷 Observable 的典型行为:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon");
//first observer
source.subscribe(s -> System.out.println("Observer 1 Received:
" + s));
//second observer
source.subscribe(s -> System.out.println("Observer 2 Received:
" + s));
}
}
输出如下:
Observer 1 Received: Alpha
Observer 1 Received: Beta
Observer 1 Received: Gamma
Observer 1 Received: Delta
Observer 1 Received: Epsilon
Observer 2 Received: Alpha
Observer 2 Received: Beta
Observer 2 Received: Gamma
Observer 2 Received: Delta
Observer 2 Received: Epsilon
即使第二个 Observer 使用操作符转换其发射,它仍然会得到自己的发射流。对冷 Observable 使用如 map() 和 filter() 这样的操作符仍然会保持产生的观察量的冷性质:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon");
//first observer
source.subscribe(s -> System.out.println("Observer 1 Received:
" + s));
//second observer
source.map(String::length).filter(i -> i >= 5)
.subscribe(s -> System.out.println("Observer 2 Received: " +
s));
}
}
输出如下:
Observer 1 Received: Alpha
Observer 1 Received: Beta
Observer 1 Received: Gamma
Observer 1 Received: Delta
Observer 1 Received: Epsilon
Observer 2 Received: 5
Observer 2 Received: 5
Observer 2 Received: 5
Observer 2 Received: 7
如前所述,发射有限数据集的 Observable 源通常是冷的。
这里有一个更贴近现实世界的例子:Dave Moten 的 RxJava-JDBC (github.com/davidmoten/rxjava-jdbc) 允许你基于 SQL 数据库查询创建冷观察量。我们不会过多地深入这个库,但如果你想要查询 SQLite 数据库,例如,在你的项目中包含 SQLite JDBC 驱动和 RxJava-JDBC 库。然后你可以以响应式的方式查询数据库表,如下面的代码片段所示:
import com.github.davidmoten.rx.jdbc.ConnectionProviderFromUrl;
import com.github.davidmoten.rx.jdbc.Database;
import rx.Observable;
import java.sql.Connection;
public class Launcher {
public static void main(String[] args) {
Connection conn =
new ConnectionProviderFromUrl("jdbc:sqlite:/home/thomas
/rexon_metals.db").get();
Database db = Database.from(conn);
Observable<String> customerNames =
db.select("SELECT NAME FROM CUSTOMER")
.getAs(String.class);
customerNames.subscribe(s -> System.out.println(s));
}
}
输出如下:
LITE Industrial
Rex Tooling Inc
Re-Barre Construction
Prairie Construction
Marsh Lane Metal Works
这个由 SQL 驱动的 Observable 是冷的。许多来自有限数据源(如数据库、文本文件或 JSON)的 Observable 都是冷的。仍然重要的是要注意源 Observable 的架构。RxJava-JDBC 将为每个 Observer 运行查询。这意味着如果在两次订阅之间数据发生变化,第二个 Observer 将获得与第一个不同的输出。但由于 Observable 正在重新执行查询(即使结果数据从底层表中更改),它仍然是冷的。
再次强调,冷观察者将以某种形式重复操作以向每个 Observer 生成这些输出。接下来,我们将介绍更类似于事件而不是数据的 热观察者。
热观察者
你刚刚了解了冷 Observable,它的工作方式类似于音乐 CD。热 Observable 更像是一个广播电台。它同时向所有观察者广播相同的输出。如果观察者订阅了一个热 Observable,接收了一些输出,然后另一个观察者随后加入,那么第二个观察者将错过那些输出。就像广播电台一样,如果你调得太晚,就会错过那首歌。
从逻辑上讲,热观察者通常表示事件而不是有限的数据集。事件可以携带数据,但存在一个时间敏感的组件,晚到的观察者可能会错过之前发出的数据。
例如,JavaFX 或 Android UI 事件可以表示为一个热 Observable。在 JavaFX 中,你可以使用 Observable.create() 创建一个基于 ToggleButton 的 selectedProperty() 操作符的 Observable<Boolean>。然后,你可以将布尔输出转换为表示 ToggleButton 是 UP 还是 DOWN 的字符串,并使用 Observer 在 Label 中显示它们,如下面的代码片段所示:
import io.reactivex.Observable;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MyJavaFxApp extends Application {
@Override
public void start(Stage stage) throws Exception {
ToggleButton toggleButton = new ToggleButton("TOGGLE ME");
Label label = new Label();
Observable<Boolean> selectedStates =
valuesOf(toggleButton.selectedProperty());
selectedStates.map(selected -> selected ? "DOWN" : "UP")
.subscribe(label::setText);
VBox vBox = new VBox(toggleButton, label);
stage.setScene(new Scene(vBox));
stage.show();
}
private static <T> Observable<T> valuesOf(final
ObservableValue<T> fxObservable) {
return Observable.create(observableEmitter -> {
//emit initial state
observableEmitter.onNext(fxObservable.getValue());
//emit value changes uses a listener
final ChangeListener<T> listener = (observableValue, prev,
current) -> observableEmitter.onNext(current);
fxObservable.addListener(listener);
});
}
}

由 ToggleButton 的选择状态创建的热 Observable<Boolean> 支持的 JavaFX 应用
注意,如果你使用 OpenJDK,你需要单独获取 JavaFX 库。最简单的方法是使用 Oracle 的官方 JDK,它包含 JavaFX,可在 www.oracle.com/technetwork/java/javase/downloads/index.html 获取。
JavaFX 的 ObservableValue 与 RxJava 的 Observable 没有任何关系。它是 JavaFX 的专有技术,但我们可以通过之前实现的 valuesOf() 工厂方法轻松地将它转换成 RxJava 的 Observable,并将 ChangeListener 作为 onNext() 调用挂钩。每次点击 ToggleButton,Observable<Boolean> 都会发出 true 或 false,反映选择状态。这是一个简单示例,说明这个 Observable 正在发出事件,同时也以 true 或 false 的形式发出数据。它将布尔值转换为字符串,并让观察者修改 Label 的文本。
在这个 JavaFX 示例中,我们只有一个观察者。如果我们在这个ToggleButton的事件发生后引入更多的观察者,那些新的观察者将错过这些事件。
JavaFX 和 Android 上的 UI 事件是热 Observables 的典型例子,但你也可以使用热 Observables 来反映服务器请求。如果你创建了一个从实时 Twitter 流中发出特定主题推文的Observable,那也将是一个热Observable。所有这些来源都可能无限,虽然许多热 Observables 确实是无限的,但它们不必是无限的。它们只需同时将事件共享给所有观察者,并且不会为迟到的观察者重新播放错过的事件。
注意,RxJavaFX(以及在第十一章中介绍的 RxAndroid,RxJava 在 Android 上)有将各种 UI 事件转换为 Observables 的工厂和绑定。使用 RxJavaFX,你可以使用valuesOf()工厂简化前面的示例。
注意,我们在这个 JavaFX 示例中确实留下了一个悬而未决的问题,因为我们从未处理过销毁。我们将在本章末尾介绍 Disposables 时重新讨论这个问题。
ConnectableObservable
热 Observables 的一种有用形式是ConnectableObservable。它可以将任何Observable(即使是冷的)转换为热 Observables,以便所有事件一次播放给所有观察者。为此转换,你只需在任意的Observable上调用publish(),它将产生一个ConnectableObservable。但是订阅不会立即开始事件发射。你需要调用它的connect()方法来开始发射事件。这允许你事先设置好所有的观察者。看看下面的代码片段:
import io.reactivex.Observable;
import io.reactivex.observables.ConnectableObservable;
public class Launcher {
public static void main(String[] args) {
ConnectableObservable<String> source =
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon")
.publish();
//Set up observer 1
source.subscribe(s -> System.out.println("Observer 1: " + s));
//Set up observer 2
source.map(String::length)
.subscribe(i -> System.out.println("Observer 2: " + i));
//Fire!
source.connect();
}
}
看看下面的代码:
Observer 1: Alpha
Observer 2: 5
Observer 1: Beta
Observer 2: 4
Observer 1: Gamma
Observer 2: 5
Observer 1: Delta
Observer 2: 5
Observer 1: Epsilon
Observer 2: 7
注意,一个观察者接收字符串,而另一个接收长度,并且它们以交错的方式打印它们。这两个订阅都在之前设置好,然后调用connect()来触发事件。不是观察者 1在观察者 2之前处理所有事件,每个事件同时发送给每个观察者。观察者 1接收Alpha,观察者 2接收5,然后是Beta和4,依此类推。使用ConnectableObservable强制每个事件同时发送给所有观察者称为多播,我们将在第五章多播中详细讨论。
ConnectableObservable有助于防止将数据重新播放给每个观察者。你可能想这样做,如果重新播放发射很昂贵,你更愿意一次性将它们发射给所有观察者。你也可能只是想强制上游操作符使用单个流实例,即使下游有多个观察者。通常,多个观察者会导致上游有多个流实例,但使用publish()返回ConnectableObservable将publish()之前的所有上游操作合并成一个单一的流。再次强调,这些细微差别将在第五章,多播中更详细地介绍。
目前,请记住ConnectableObservable是热的,因此,如果在调用connect()之后发生新的订阅,它们将错过之前发射的发射。
其他 Observable 源
我们已经介绍了一些创建Observable源的工厂,包括Observable.create()、Observable.just()和Observable.fromIterable()。在介绍观察者和它们的细微差别之后,让我们继续之前的话题,并介绍一些更多的Observable工厂。
Observable.range()
要发射一个连续的整数范围,你可以使用Observable.range()。这将从起始值开始发射每个数字,并在达到指定的计数之前递增每个发射。这些数字都通过onNext()事件传递,然后是onComplete()事件:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10)
.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
输出如下:
RECEIVED: 1
RECEIVED: 2
RECEIVED: 3
RECEIVED: 4
RECEIVED: 5
RECEIVED: 6
RECEIVED: 7
RECEIVED: 8
RECEIVED: 9
RECEIVED: 10
仔细注意,Observable.range()的两个参数不是下限/上限。第一个参数是起始值。第二个参数是发射的总数,这包括初始值和递增的值。尝试发射Observable.range(5,10),你会注意到它首先发射5,然后是接下来的九个连续整数(总共发射 10 次):
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(5,10)
.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
输出如下:
RECEIVED: 5
RECEIVED: 6
RECEIVED: 7
RECEIVED: 8
RECEIVED: 9
RECEIVED: 10
RECEIVED: 11
RECEIVED: 12
RECEIVED: 13
RECEIVED: 14
注意,如果你需要发射更大的数字,还有一个长等效函数Observable.rangeLong()。
Observable.interval()
正如我们所见,Observables 有一个随时间发射的概念。发射是按顺序从源传递到观察者的。但是,这些发射可以根据源提供它们的时间来分散。我们的 JavaFX 示例使用ToggleButton展示了这一点,因为每次点击都会导致发射true或false。
但让我们看看使用Observable.interval()的一个基于时间的Observable的简单示例。它将在每个指定的时间间隔发射一个连续的长整数发射(从0开始)。在这里,我们有一个每秒发射一次的Observable<Long>:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[]args) {
Observable.interval(1, TimeUnit.SECONDS)
.subscribe(s -> System.out.println(s + " Mississippi"));
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
0 Mississippi
1 Mississippi
2 Mississippi
3 Mississippi
4 Mississippi
Observable.interval()将以指定的间隔无限发射(在本例中为 1 秒)。然而,因为它在计时器上操作,所以需要在单独的线程上运行,并且默认情况下将在计算调度器上运行。我们将在第六章中介绍并发和并行化,并学习调度器。现在,只需注意我们的main()方法将启动这个Observable,但它不会等待它完成。现在它在一个单独的线程上发射。为了防止我们的main()方法在Observable有机会发射之前完成并退出应用程序,我们使用sleep()方法使这个应用程序保持活跃五秒钟。这给我们的Observable五秒钟的时间来发射,然后应用程序退出。当您创建生产应用程序时,您可能很少会遇到这个问题,因为像网络服务、Android 应用程序或 JavaFX 这样的非守护线程将保持应用程序的活跃状态。
玩笑问题:Observable.interval()返回的是热可观察对象还是冷可观察对象?因为它是由事件驱动的(并且是无限的),您可能会想它是热的。但是,在它上面添加第二个Observer,等待五秒,然后添加另一个观察者。会发生什么?让我们看看:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> seconds = Observable.interval(1,
TimeUnit.SECONDS);
//Observer 1
seconds.subscribe(l -> System.out.println("Observer 1: " + l));
//sleep 5 seconds
sleep(5000);
//Observer 2
seconds.subscribe(l -> System.out.println("Observer 2: " + l));
//sleep 5 seconds
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 1: 4
Observer 1: 5
Observer 2: 0
Observer 1: 6
Observer 2: 1
Observer 1: 7
Observer 2: 2
Observer 1: 8
Observer 2: 3
Observer 1: 9
Observer 2: 4
看看五秒后发生了什么,当Observer 2到来时。注意,它有自己的单独计时器,从0开始!这两个观察者实际上是在获取它们自己的发射,每个都是从0开始的。所以这个Observable实际上是冷的。为了将所有观察者放在同一个计时器上,并具有相同的发射,您将想要使用ConnectableObservable来强制这些发射变为热:
import io.reactivex.Observable;
import io.reactivex.observables.ConnectableObservable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
ConnectableObservable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS).publish();
//observer 1
seconds.subscribe(l -> System.out.println("Observer 1: " + l));
seconds.connect();
//sleep 5 seconds
sleep(5000);
//observer 2
seconds.subscribe(l -> System.out.println("Observer 2: " + l));
//sleep 5 seconds
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 1: 4
Observer 1: 5
Observer 2: 5
Observer 1: 6
Observer 2: 6
Observer 1: 7
Observer 2: 7
Observer 1: 8
Observer 2: 8
Observer 1: 9
Observer 2: 9
现在,Observer 2虽然晚了 5 秒并且错过了之前的发射,但至少与Observer 1完全同步,并接收到了相同的发射。
Observable.future()
RxJava 的可观察对象(Observables)比Futures更加健壮和表达性强,但如果您有现有的库返回Futures,您可以通过Observable.future()轻松地将它们转换为可观察对象:
import io.reactivex.Observable;
import java.util.concurrent.Future;
public class Launcher {
public static void main(String[] args) {
Future<String> futureValue = ...;
Observable.fromFuture(futureValue)
.map(String::length)
.subscribe(System.out::println);
}
}
Observable.empty()
虽然这可能看起来目前没有太大用处,但有时创建一个不发射任何内容并调用onComplete()的可观察对象是有帮助的:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> empty = Observable.empty();
empty.subscribe(System.out::println,
Throwable::printStackTrace,
() -> System.out.println("Done!"));
}
}
输出结果如下:
Done!
注意,没有打印任何发射,因为没有发射。它直接调用onComplete,在观察者中打印了Done!消息。空的可观察对象常用于表示空数据集。它们也可能由filter()等操作符产生,当所有发射都未能满足条件时。有时,您会故意使用Observable.empty()创建空的可观察对象,我们将在本书的几个地方看到这个例子。
一个空的Observable基本上是 RxJava 的 null 概念。它是值的缺失(或者技术上,“值”)。空的Observable比 null 更优雅,因为操作将简单地继续为空而不是抛出NullPointerExceptions。但是,当 RxJava 程序中的事情出错时,有时是因为观察者没有收到任何发射物。当这种情况发生时,您必须追踪您的Observable的运算符链以找到导致发射物变为空的运算符。
Observable.never()
Observable.empty()的一个近亲是Observable.never()。它们之间的唯一区别是它永远不会调用onComplete(),永远让观察者等待发射物,但永远不会实际提供任何:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> empty = Observable.never();
empty.subscribe(System.out::println,
Throwable::printStackTrace,
() -> System.out.println("Done!"));
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这个Observable主要用于测试,在生产环境中并不常用。我们在这里必须使用sleep(),就像使用Observable.interval()一样,因为启动后主线程不会等待它。在这种情况下,我们只使用sleep()五秒钟来证明没有发射物来自它。然后,应用程序将退出。
Observable.error()
这也是您可能只会用于测试的事情,但您可以创建一个立即调用onError()并带有指定异常的Observable:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.error(new Exception("Crash and burn!"))
.subscribe(i -> System.out.println("RECEIVED: " + i),
Throwable::printStackTrace,
() -> System.out.println("Done!"));
}
}
输出如下:
java.lang.Exception: Crash and burn!
at Launcher.lambda$main$0(Launcher.java:7)
at io.reactivex.internal.operators.observable.
ObservableError.subscribeActual(ObservableError.java:32)
at io.reactivex.Observable.subscribe(Observable.java:10514)
at io.reactivex.Observable.subscribe(Observable.java:10500)
...
您也可以通过 lambda 表达式提供异常,这样它就是从头创建的,并且为每个观察者提供单独的异常实例:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.error(() -> new Exception("Crash and burn!"))
.subscribe(i -> System.out.println("RECEIVED: " + i),
Throwable::printStackTrace,
() -> System.out.println("Done!"));
}
}
Observable.defer()
Observable.defer()是一个强大的工厂,因为它能够为每个Observer创建一个单独的状态。当使用某些Observable工厂时,如果您的源是状态性的并且您想要为每个Observer创建一个单独的状态,您可能会遇到一些细微差别。您的源Observable可能没有捕捉到其参数的变化,并发送了过时的发射物。这里有一个简单的例子:我们有一个基于两个静态int属性start和count的Observable.range()。
如果您订阅了这个Observable,修改计数,然后再次订阅,您会发现第二个Observer看不到这个变化:
import io.reactivex.Observable;
public class Launcher {
private static int start = 1;
private static int count = 5;
public static void main(String[] args) {
Observable<Integer> source = Observable.range(start,count);
source.subscribe(i -> System.out.println("Observer 1: " + i));
//modify count
count = 10;
source.subscribe(i -> System.out.println("Observer 2: " + i));
}
}
输出如下:
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 1: 4
Observer 1: 5
Observer 2: 1
Observer 2: 2
Observer 2: 3
Observer 2: 4
Observer 2: 5
为了解决Observable源没有捕捉状态变化的问题,您可以为每个订阅创建一个新的Observable。这可以通过使用Observable.defer()来实现,它接受一个 lambda 表达式,指示如何为每个订阅创建一个Observable。因为每次都会创建一个新的Observable,所以它会反映其参数的任何变化:
import io.reactivex.Observable;
public class Launcher {
private static int start = 1;
private static int count = 5;
public static void main(String[] args) {
Observable<Integer> source = Observable.defer(() ->
Observable.range(start,count));
source.subscribe(i -> System.out.println("Observer 1: " + i));
//modify count
count = 10;
source.subscribe(i -> System.out.println("Observer 2: " + i));
}
}
输出如下:
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 1: 4
Observer 1: 5
Observer 2: 1
Observer 2: 2
Observer 2: 3
Observer 2: 4
Observer 2: 5
Observer 2: 6
Observer 2: 7
Observer 2: 8
Observer 2: 9
Observer 2: 10
这样更好!当您的Observable源没有捕捉到驱动它的东西的变化时,尝试将其放入Observable.defer()。如果您的Observable源实现得过于简单并且与多个观察者(例如,它重用只迭代数据一次的迭代器)一起表现不佳,Observable.defer()也提供了一个快速解决方案。
Observable.fromCallable()
如果你需要执行一个计算或操作然后发射它,你可以使用 Observable.just()(或 Single.just() 或 Maybe.just(),我们将在后面学习),但有时我们希望以懒或延迟的方式执行。此外,如果该过程抛出错误,我们希望它通过 onError() 发射到 Observable 链而不是在传统的 Java 风格中将错误抛出在那个位置。例如,如果你尝试将 Observable.just() 包裹在一个除以 0 的表达式中,异常将被抛出,而不是发射到 Observer:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(1 / 0)
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("Error Captured: " + e));
}
}
输出如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Launcher.main(Launcher.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke
(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.
invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.
application.AppMain.main(AppMain.java:147)
如果我们打算在错误处理方面做出反应,这可能不是所希望的。也许你希望错误被发射到链中的 Observer,在那里它将被处理。如果是这种情况,请使用 Observable.fromCallable(),因为它接受一个 lambda Supplier<T>,并且它将发射任何发生到 Observer 的错误:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.fromCallable(() -> 1 / 0)
.subscribe(i -> System.out.println("Received: " + i),
e -> System.out.println("Error Captured: " + e));
}
}
输出如下:
Error Captured: java.lang.ArithmeticException: / by zero
这样更好!错误被发射到了 Observer,而不是在发生错误的地方抛出。如果你初始化发射有抛出错误的可能性,你应该使用 Observable.fromCallable() 而不是 Observable.just()。
Single, Completable, and Maybe
有几种专门的 Observable 类型是明确为单个或零个发射而设置的:Single、Maybe 和 Completable。这些都与 Observable 非常接近,并且应该在你的响应式编码工作流程中使用起来很直观。你可以用类似 Observable 的方式创建它们(例如,它们各自都有自己的 create() 工厂),但某些 Observable 操作符也可能返回它们。
Single
Single<T> 实质上是一个只发射一个项目的 Observable<T>。它的工作方式与可观察者类似,但它仅限于对单个发射有意义的操作符。它还有一个自己的 SingleObserver 接口:
interface SingleObserver<T> {
void onSubscribe(Disposable d);
void onSuccess(T value);
void onError(Throwable error);
}
onSuccess() 实质上将 onNext() 和 onComplete() 合并成一个接受一个发射的单个事件。当你对一个 Single 调用 subscribe() 时,你提供 onSuccess() 的 lambda 以及可选的 onError():
import io.reactivex.Single;
public class Launcher {
public static void main(String[] args) {
Single.just("Hello")
.map(String::length)
.subscribe(System.out::println,
Throwable::printStackTrace);
}
}
某些 RxJava 可观察者操作符将产生一个 Single,我们将在下一章中看到。例如,first() 操作符将返回一个 Single,因为该操作符在逻辑上只关注单个项目。然而,如果可观察者返回空值,它将接受一个默认值作为参数(以下示例中我将其指定为 Nil):
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha","Beta","Gamma");
source.first("Nil") //returns a Single
.subscribe(System.out::println);
}
}
输出如下:
Alpha
Single 必须有一个发射,如果你只有一个发射要提供,你应该优先选择它。这意味着,而不是使用 Observable.just("Alpha"),你应该尝试使用 Single.just("Alpha")。Single 上有操作符可以将它转换为 Observable,例如 toObservable()。
如果有 0 或 1 个发射,你将想要使用 Maybe。
Maybe
Maybe 与 Single 类似,但它允许根本不发生事件(因此称为 Maybe)。MaybeObserver 与标准观察者非常相似,但 onNext() 被称为 onSuccess():
public interface MaybeObserver<T> {
void onSubscribe(Disposable d);
void onSuccess(T value);
void onError(Throwable e);
void onComplete();
}
给定的 Maybe<T> 只会发出 0 或 1 个事件。它会将可能的事件传递给 onSuccess(),并在完成时调用 onComplete()。Maybe.just() 可以用来创建发出单个项的 Maybe,而 Maybe.empty() 将创建一个不产生任何事件的 Maybe:
import io.reactivex.Maybe;
public class Launcher {
public static void main(String[] args) {
// has emission
Maybe<Integer> presentSource = Maybe.just(100);
presentSource.subscribe(s -> System.out.println("Process 1
received: " + s),
Throwable::printStackTrace,
() -> System.out.println("Process 1 done!"));
//no emission
Maybe<Integer> emptySource = Maybe.empty();
emptySource.subscribe(s -> System.out.println("Process 2
received: " + s),
Throwable::printStackTrace,
() -> System.out.println("Process 2 done!"));
}
}
输出如下:
Process 1 received: 100
Process 2 done!
我们稍后将要学习的某些 Observable 操作符会返回一个 Maybe。一个例子是 firstElement() 操作符,它与 first() 类似,但如果没有任何事件被发射,它将返回一个空的结果:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon");
source.firstElement().subscribe(
s -> System.out.println("RECEIVED " + s),
Throwable::printStackTrace,
() -> System.out.println("Done!"));
}
}
输出如下:
RECEIVED Alpha
可完成性
Completable 仅关注一个动作的执行,但它不会接收任何事件。从逻辑上讲,它没有 onNext() 或 onSuccess() 来接收事件,但它确实有 onError() 和 onComplete():
interface CompletableObserver<T> {
void onSubscribe(Disposable d);
void onComplete();
void onError(Throwable error);
}
Completable 是你可能不会经常使用的东西。你可以通过调用 Completable.complete() 或 Completable.fromRunnable() 快速构建一个。前者会立即调用 onComplete() 而不执行任何操作,而 fromRunnable() 将在调用 onComplete() 之前执行指定的动作:
import io.reactivex.Completable;
public class Launcher {
public static void main(String[] args) {
Completable.fromRunnable(() -> runProcess())
.subscribe(() -> System.out.println("Done!"));
}
public static void runProcess() {
//run process here
}
}
输出如下:
Done!
清理
当你订阅一个 Observable 来接收事件时,会创建一个流来处理这些事件通过 Observable 链。当然,这会使用资源。当我们完成时,我们想要清理这些资源,以便它们可以被垃圾回收。幸运的是,调用 onComplete() 的有限 Observable 通常会在完成时安全地清理自己。但是,如果你正在处理无限或长时间运行的 Observable,你可能会遇到想要明确停止事件发射并清理与该订阅相关的一切的情况。实际上,你不能依赖垃圾回收器来处理不再需要的活动订阅,显式清理是防止内存泄漏所必需的。
Disposable 是 Observable 和活动观察者之间的一个链接,你可以调用它的 dispose() 方法来停止事件发射并清理用于该观察者的所有资源。它还有一个 isDisposed() 方法,指示它是否已经被清理:
package io.reactivex.disposables;
public interface Disposable {
void dispose();
boolean isDisposed();
}
当你将 onNext()、onComplete() 和/或 onError() 作为参数传递给 subscribe() 方法时,它实际上会返回一个 Disposable。你可以通过调用其 dispose() 方法在任何时候停止事件发射。例如,我们可以在五秒后停止接收来自 Observable.interval() 的发射:
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS);
Disposable disposable =
seconds.subscribe(l -> System.out.println("Received: " + l));
//sleep 5 seconds
sleep(5000);
//dispose and stop emissions
disposable.dispose();
//sleep 5 seconds to prove
//there are no more emissions
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这里,我们让 Observable.interval() 在一个观察者中运行五秒钟,但我们保存了从 subscribe() 方法返回的 Disposable。然后我们调用 Disposable 的 dispose() 方法来停止进程并释放任何正在使用的资源。然后,我们再睡五秒钟,只是为了证明没有更多的发射发生。
在观察者内部处理销毁
之前,我回避了在 Observer 中谈论 onSubscribe() 方法,但现在我们将解决这个问题。你可能已经注意到,Disposable 是通过 onSubscribe() 方法在 Observer 的实现中传递的。这个方法是在 RxJava 2.0 中添加的,它允许 Observer 在任何时候都有权取消订阅。
例如,你可以实现自己的 Observer 并使用 onNext()、onComplete() 或 onError() 来访问 Disposable。这样,这三个事件可以在 Observer 不想接收更多发射的任何原因下调用 dispose():
Observer<Integer> myObserver = new Observer<Integer>() {
private Disposable disposable;
@Override
public void onSubscribe(Disposable disposable) {
this.disposable = disposable;
}
@Override
public void onNext(Integer value) {
//has access to Disposable
}
@Override
public void onError(Throwable e) {
//has access to Disposable
}
@Override
public void onComplete() {
//has access to Disposable
}
};
Disposable 从源头一路传递到观察者,所以 Observable 链中的每一步都有权访问 Disposable。
注意,将 Observer 传递给 subscribe() 方法将无效,并且不会返回 Disposable,因为假设 Observer 将处理它。如果你不想显式处理 Disposable,并希望 RxJava 为你处理(这可能是一个好主意,直到你有理由接管),你可以扩展 ResourceObserver 作为你的观察者,它使用默认的 Disposable 处理。将此传递给 subscribeWith() 而不是 subscribe(),你将获得默认的 Disposable 返回:
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import io.reactivex.observers.ResourceObserver;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> source =
Observable.interval(1, TimeUnit.SECONDS);
ResourceObserver<Long> myObserver = new
ResourceObserver<Long>() {
@Override
public void onNext(Long value) {
System.out.println(value);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done!");
}
};
//capture Disposable
Disposable disposable = source.subscribeWith(myObserver);
}
}
使用 CompositeDisposable
如果你需要管理并销毁多个订阅,使用 CompositeDisposable 可能会有所帮助。它实现了 Disposable,但内部持有 disposables 的集合,你可以添加到其中,然后一次性销毁所有:
import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.util.concurrent.TimeUnit;
public class Launcher {
private static final CompositeDisposable disposables
= new CompositeDisposable();
public static void main(String[] args) {
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS);
//subscribe and capture disposables
Disposable disposable1 =
seconds.subscribe(l -> System.out.println("Observer 1: " +
l));
Disposable disposable2 =
seconds.subscribe(l -> System.out.println("Observer 2: " +
l));
//put both disposables into CompositeDisposable
disposables.addAll(disposable1, disposable2);
//sleep 5 seconds
sleep(5000);
//dispose all disposables
disposables.dispose();
//sleep 5 seconds to prove
//there are no more emissions
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CompositeDisposable 是一个简单但有用的实用工具,可以维护一个你可以通过调用 add() 或 addAll() 添加的 disposables 集合。当你不再需要这些订阅时,你可以调用 dispose() 一次性销毁所有这些。
使用 Observable.create() 处理销毁
如果你的 Observable.create() 返回一个长时间运行或无限的 Observable,你应该理想地定期检查 ObservableEmitter 的 isDisposed() 方法,以查看你是否应该继续发送发射。这可以防止在订阅不再活跃时进行不必要的操作。
在这种情况下,你应该使用 Observable.range(),但为了举例,让我们假设我们在 Observable.create() 中的 for 循环中发射整数。在发射每个整数之前,你应该确保 ObservableEmitter 没有指示已经调用销毁:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> source =
Observable.create(observableEmitter -> {
try {
for (int i = 0; i < 1000; i++) {
while (!observableEmitter.isDisposed()) {
observableEmitter.onNext(i);
}
if (observableEmitter.isDisposed())
return;
}
observableEmitter.onComplete();
} catch (Throwable e) {
observableEmitter.onError(e);
}
});
}
}
如果你的 Observable.create() 包围了一些资源,你也应该处理该资源的释放以防止泄漏。ObservableEmitter 有 setCancellable() 和 setDisposable() 方法来处理。在我们的早期 JavaFX 示例中,当发生释放操作时,我们应该从我们的 JavaFX ObservableValue 中移除 ChangeListener。我们可以提供一个 lambda 表达式给 setCancellable(),它将执行以下操作,当调用 dispose() 时会发生:
private static <T> Observable<T> valuesOf(final ObservableValue<T>
fxObservable) {
return Observable.create(observableEmitter -> {
//emit initial state
observableEmitter.onNext(fxObservable.getValue());
//emit value changes uses a listener
final ChangeListener<T> listener =
(observableValue, prev, current) ->
observableEmitter.onNext(current);
//add listener to ObservableValue
fxObservable.addListener(listener);
//Handle disposing by specifying cancellable
observableEmitter.setCancellable(() ->
fxObservable.removeListener(listener));
});
}
摘要
这是一个充满挑战的章节,但当你学习如何使用 RxJava 来处理现实世界的工作时,它将为你提供一个坚实的基础。RxJava,凭借其强大的表达能力,有一些细微之处完全是因为它要求的心态转变。它已经做了令人印象深刻的工作,将像 Java 这样的命令式语言适应成反应式和函数式。但这种互操作性需要理解 Observable 和 Observer 之间的实现细节。我们讨论了创建 Observables 的各种方法以及它们如何与 Observers 交互。
仔细消化所有这些信息,但不要让它阻止你继续阅读下一章,在那里 RxJava 的实用性开始形成。在下一章中,RxJava 的实用价值将开始变得清晰。
第三章:基本操作符
在上一章中,你学到了很多关于 Observable 和 Observer 的知识。我们还介绍了一些操作符,特别是 map() 和 filter(),以了解操作符的作用。但我们可以利用数百个 RxJava 操作符来表达业务逻辑和行为。我们将在这本书的大部分内容中全面介绍操作符,这样你知道何时使用哪些操作符。了解可用的操作符并将它们组合起来对于成功使用 ReactiveX 至关重要。你应该努力使用操作符来表达业务逻辑,以便你的代码尽可能保持反应式。
应该注意的是,操作符本身是它们所调用的 Observable 的观察者。如果你在 Observable 上调用 map(),返回的 Observable 将订阅它。然后它将转换每个输出,并依次成为下游观察者(包括其他操作符和终端 Observer)的生产者。
你应该努力使用 RxJava 操作符执行尽可能多的逻辑,并使用 Observer 来接收准备消费的最终产品输出。尽量别从 Observable 链中提取值,或者求助于阻塞过程或命令式编程策略。当你保持算法和过程反应式时,你可以轻松利用反应式编程的好处,如降低内存使用、灵活的并发性和可丢弃性。
在本章中,我们将介绍以下主题:
-
抑制操作符
-
转换操作符
-
减法操作符
-
错误恢复操作符
-
动作操作符
抑制操作符
有许多操作符可以抑制不符合特定标准的输出。这些操作符通过简单地不对不合格的输出调用下游的 onNext() 函数来实现,因此不会向下传递到 Observer。我们已经看到了 filter() 操作符,这可能是最常见的抑制操作符。我们将从这个开始。
filter()
filter() 操作符接受 Predicate<T> 用于给定的 Observable<T>。这意味着你提供一个 lambda 表达式,通过映射每个输出到布尔值来验证每个输出,带有 false 的输出将不会继续。
例如,你可以使用 filter() 来只允许长度不是五个字符的字符串输出:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.filter(s -> s.length() != 5)
subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
上述代码片段的输出如下:
RECEIVED: Beta
RECEIVED: Epsilon
filter() 函数可能是最常用的用于抑制输出的操作符。
注意,如果所有输出都无法满足你的标准,返回的 Observable 将为空,且在调用 onComplete() 之前不会发生任何输出。
take()
take()操作符有两个重载版本。其中一个将获取指定数量的发射,并在捕获所有这些发射后调用onComplete()。它还将销毁整个订阅,以便不再发生更多发射。例如,take(3)将发射前三个发射,然后调用onComplete()事件:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.take(3)
.subscribe(s -> System.out.println("RECEIVED: " + s));
}
}
上一代码片段的输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Gamma
注意,如果你在take()函数中指定的发射少于你得到的,它将简单地发射它所得到的,然后调用onComplete()函数。
另一个重载版本将获取特定时间持续时间内的发射,然后调用onComplete()。当然,我们这里的冷Observable会非常快地发射,这会是一个不好的例子。也许使用Observable.interval()函数会更好。让我们以每300毫秒发射一次,但在以下代码片段中只对2秒内的发射进行take()操作:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(300, TimeUnit.MILLISECONDS)
.take(2, TimeUnit.SECONDS)
.subscribe(i -> System.out.println("RECEIVED: " + i));
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上一代码片段的输出如下:
RECEIVED: 0
RECEIVED: 1
RECEIVED: 2
RECEIVED: 3
RECEIVED: 4
RECEIVED: 5
你可能会得到这里显示的输出(每个打印每300毫秒发生一次)。如果它们以300毫秒的间隔分散,那么在2秒内你只能得到六个发射。
注意,还有一个takeLast()操作符,它将在调用onComplete()函数之前获取指定数量的最后发射(或时间持续时间)。只需记住,它将内部排队发射,直到其onComplete()函数被调用,然后它可以逻辑上识别并发射最后的发射。
skip()
skip()操作符与take()操作符相反。它将忽略指定数量的发射,然后发射后续的发射。如果我想跳过一个Observable的前90个发射,我可以使用这个操作符,如下所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,100)
.skip(90)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
以下代码片段的输出如下:
RECEIVED: 91
RECEIVED: 92
RECEIVED: 93
RECEIVED: 94
RECEIVED: 95
RECEIVED: 96
RECEIVED: 97
RECEIVED: 98
RECEIVED: 99
RECEIVED: 100
就像take()操作符一样,还有一个接受时间持续的重载。还有一个skipLast()操作符,它将在调用onComplete()事件之前跳过指定数量的项目(或时间持续时间)。只需记住,skipLast()操作符将排队并延迟发射,直到它确认该范围内的最后发射。
takeWhile() 和 skipWhile()
take()操作符的另一个变体是takeWhile()操作符,它将在从每个发射推导出的条件为真时获取发射。以下示例将在发射小于5时持续获取发射。一旦遇到一个不满足条件的发射,它将调用onComplete()函数并销毁它:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,100)
.takeWhile(i -> i < 5)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上一代码片段的输出如下:
RECEIVED: 1
RECEIVED: 2
RECEIVED: 3
RECEIVED: 4
就像takeWhile()函数一样,还有一个skipWhile()函数。它将一直跳过满足条件的排放项。一旦条件不再满足,排放项将开始通过。在下面的代码中,只要排放项小于或等于95,就跳过排放项。一旦遇到不满足此条件的排放项,它将允许所有后续的排放项向前传递:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,100)
.skipWhile(i -> i <= 95)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码片段的输出如下:
RECEIVED: 96
RECEIVED: 97
RECEIVED: 98
RECEIVED: 99
RECEIVED: 100
takeUntil()操作符与takeWhile()类似,但它接受另一个Observable作为参数。它将一直接收排放项,直到那个其他Observable推送一个排放项。skipUntil()操作符具有类似的行为。它也接受另一个Observable作为参数,但它将一直跳过,直到其他Observable发出一些内容。
distinct()
distinct()操作符会排放每个唯一的排放项,但它会抑制随后出现的任何重复项。等价性基于排放对象的hashCode()/equals()实现。如果我们想排放字符串序列的唯一长度,可以这样做:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.map(String::length)
.distinct()
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码片段的输出如下:
RECEIVED: 5
RECEIVED: 4
RECEIVED: 7
请记住,如果您有一系列广泛的、独特的值,distinct()可能会使用一些内存。想象一下,每个订阅都会产生一个HashSet,用于跟踪之前捕获的唯一值。
您还可以添加一个 lambda 参数,将每个排放项映射到一个用于等价逻辑的键。这允许排放项(而不是键)向前传递,同时使用键进行不同的逻辑。例如,我们可以根据每个字符串的长度进行键控,并使用它来保证唯一性,但排放字符串而不是它们的长度:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.distinct(String::length)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码片段的输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Epsilon
Alpha是五个字符,Beta是四个。Gamma和Delta被忽略,因为Alpha已经被排放,并且是 5 个字符。Epsilon是七个字符,因为还没有排放过七个字符的字符串,所以它被向前排放。
distinctUntilChanged()
distinctUntilChanged()函数会忽略连续的重复排放项。这是一种忽略重复直到它们改变的有用方法。如果正在重复排放相同的值,所有重复项都将被忽略,直到发射一个新的值。下一个值的重复项将被忽略,直到它再次改变,依此类推。观察以下代码的输出,以查看此行为:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(1, 1, 1, 2, 2, 3, 3, 2, 1, 1)
.distinctUntilChanged()
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码片段的输出如下:
RECEIVED: 1
RECEIVED: 2
RECEIVED: 3
RECEIVED: 2
RECEIVED: 1
我们首先接收一个1的排放,这是允许的。但接下来的两个1被忽略,因为它们是连续的重复项。当它切换到2时,那个初始的2被排放,但随后的重复项被忽略。排放一个3,其随后的重复项也被忽略。最后,我们切换回一个排放的2,然后是一个重复项被忽略的1。
就像distinct()一样,你可以通过一个 lambda 映射提供一个可选的键参数。在以下代码片段中,我们使用基于字符串长度的键执行distinctUntilChanged()操作:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Zeta", "Eta", "Gamma",
"Delta")
.distinctUntilChanged(String::length)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码片段的输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Eta
RECEIVED: Gamma
注意,由于Zeta紧随Beta之后,而Beta也是四个字符,因此跳过了Zeta。同样,由于Delta紧随Gamma之后,而Gamma也是五个字符,因此也忽略了Delta。
elementAt()
你可以通过指定一个 Long 类型的索引来获取特定的发射项,索引从0开始。找到并发出该项目后,将调用onComplete(),并取消订阅。
如果你想从Observable中获取第四个发射项,你可以按照以下代码片段所示进行操作:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Zeta", "Eta", "Gamma",
"Delta")
.elementAt(3)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
以下代码片段的输出如下:
RECEIVED: Eta
你可能没有注意到,elementAt()返回Maybe<T>而不是Observable<T>。这是因为它将产生一个发射项,但如果发射项少于所求索引,它将是空的。
elementAt()还有其他变体,例如elementAtOrError(),它返回一个Single,如果在该索引处找不到元素,则会发出错误。singleElement()将Observable转换为Maybe,但如果存在超过一个元素,则会产生错误。最后,firstElement()和lastElement()将分别产生第一个或最后一个发射项。
转换操作符
接下来,我们将介绍各种常见的转换发射项的操作符。在Observable链中的操作符系列是一系列转换。你已经看到了map(),这是这一类别中最明显的操作符。我们将从这个开始。
map()
对于给定的Observable<T>,map()操作符将使用提供的Function<T,R> lambda 将T发射项转换为R发射项。我们已经多次使用此操作符,将字符串转换为长度。以下是一个新示例:我们可以将原始日期字符串作为输入,并使用map()操作符将每个字符串转换为LocalDate发射项,如下所示:
import io.reactivex.Observable;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class Launcher {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("M/d
/yyyy");
Observable.just("1/3/2016", "5/9/2016", "10/12/2016")
.map(s -> LocalDate.parse(s, dtf))
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码片段的输出如下:
RECEIVED: 2016-01-03
RECEIVED: 2016-05-09
RECEIVED: 2016-10-12
我们传递了一个将每个字符串转换为LocalDate对象的 lambda 表达式。我们提前创建了一个DateTimeFormatter,以便协助进行LocalDate.parse()操作,该操作返回一个LocalDate。然后,我们将每个LocalDate发射项推送到我们的Observer以进行打印。
map()操作符对每个发射项进行一对一的转换。如果你需要执行一对一转换(将一个发射项转换为多个发射项),你可能希望使用flatMap()或concatMap(),我们将在下一章中介绍。
cast()
一个简单的、类似映射的操作符,用于将每个发射项转换为不同类型的是cast()。如果我们想将Observable<String>转换为对象(并返回一个Observable<Object>),我们可以使用map()操作符,如下所示:
Observable<Object> items =
Observable.just("Alpha", "Beta", "Gamma").map(s -> (Object) s);
但我们可以使用的一个简写是cast(),我们可以简单地传递我们想要转换到的类类型,如下面的代码片段所示:
Observable<Object> items =
Observable.just("Alpha", "Beta", "Gamma").cast(Object.class);
如果你发现由于继承或多态类型混合而出现打字问题,这是一个将所有内容强制转换为公共基类型的有效暴力方法。但首先努力正确使用泛型和适当使用类型通配符。
startWith()
对于给定的Observable<T>,startWith()操作符允许你插入一个在所有其他发射之前发生的T发射。例如,如果我们有一个Observable<String>,它发射我们想要打印的菜单项,我们可以使用startWith()来首先添加一个标题头:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> menu =
Observable.just("Coffee", "Tea", "Espresso", "Latte");
//print menu
menu.startWith("COFFEE SHOP MENU")
.subscribe(System.out::println);
}
}
上述代码片段的输出如下:
COFFEE SHOP MENU
Coffee
Tea
Espresso
Latte
如果你想要开始于多个发射,请使用startWithArray()来接受varargs参数。如果我们想在标题和菜单项之间添加分隔符,我们可以开始于标题和分隔符作为发射:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> menu =
Observable.just("Coffee", "Tea", "Espresso", "Latte");
//print menu
menu.startWithArray("COFFEE SHOP MENU","----------------")
.subscribe(System.out::println);
}
}
上述代码片段的输出如下:
COFFEE SHOP MENU
----------------
Coffee
Tea
Espresso
Latte
startWith()操作符在这种情况下很有用,当我们想要提供一个初始值或在我们自己的发射之前添加一个或多个发射。
如果你想要一个完整的Observable发射先于另一个Observable的发射,你将想要使用Observable.concat()或concatWith(),我们将在下一章中介绍。
defaultIfEmpty()
如果我们希望在给定的Observable为空时仅使用单个发射,我们可以使用defaultIfEmpty()。对于给定的Observable<T>,我们可以在调用onComplete()时没有发射发生的情况下指定一个默认的T发射。
如果我们有一个Observable<String>并过滤以查找以Z开头的项,但没有项符合此标准,我们可以求助于发射None:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> items =
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon");
items.filter(s -> s.startsWith("Z"))
.defaultIfEmpty("None")
.subscribe(System.out::println);
}
}
上述代码片段的输出如下:
None
当然,如果发生发射,我们永远不会看到None被发射。这只会在前面的Observable为空时发生。
switchIfEmpty()
与defaultIfEmpty()类似,switchIfEmpty()在源Observable为空时指定一个不同的Observable来发射值。这允许我们在源为空的情况下指定不同的发射序列,而不是只发射一个值。
我们可以选择发射三个额外的字符串,例如,如果前面的Observable由于filter()操作为空:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.filter(s -> s.startsWith("Z"))
.switchIfEmpty(Observable.just("Zeta", "Eta", "Theta"))
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上述代码片段的输出如下:
RECEIVED: Zeta
RECEIVED: Eta
RECEIVED: Theta
当然,如果前面的Observable不为空,那么switchIfEmpty()将没有效果,不会使用指定的Observable。
sorted()
如果你有一个有限制的Observable<T>,它发射实现Comparable<T>的项,你可以使用sorted()来对发射进行排序。内部,它将收集所有发射,然后按排序顺序重新发射它们。在以下代码片段中,我们按自然顺序对Observable<Integer>的发射进行排序:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(6, 2, 5, 7, 1, 4, 9, 8, 3)
.sorted()
.subscribe(System.out::println);
}
}
上述代码片段的输出如下:
1
2
3
4
5
6
7
8
9
当然,这可能会对性能产生一些影响,因为它将在再次发射之前在内存中收集所有排放。如果你使用它来对抗无限的Observable,可能会得到OutOfMemory错误。
你还可以提供Comparator作为参数来指定一个显式的排序标准。我们可以提供Comparator来反转排序顺序,如下所示:
import io.reactivex.Observable;
import java.util.Comparator;
public class Launcher {
public static void main(String[] args) {
Observable.just(6, 2, 5, 7, 1, 4, 9, 8, 3)
.sorted(Comparator.reverseOrder())
.subscribe(System.out::println);
}
}
前一个代码片段的输出如下:
9
8
7
6
5
4
3
2
1
由于Comparator是一个单抽象方法接口,你可以通过 lambda 表达式快速实现它。指定代表两个排放的两个参数,然后将它们映射到它们的比较操作。我们可以用这个来按长度排序字符串排放,例如。这也允许我们排序不实现Comparable接口的项目:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma" ,"Delta", "Epsilon")
.sorted((x,y) -> Integer.compare(x.length(), y.length()))
.subscribe(System.out::println);
}
}
前一个代码片段的输出如下:
Beta
Alpha
Gamma
Delta
Epsilon
delay()
我们可以使用delay()运算符来推迟排放。它将保留任何接收到的排放,并将每个排放推迟指定的时长。如果我们想将排放推迟三秒,可以这样做:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma" ,"Delta", "Epsilon")
.delay(3, TimeUnit.SECONDS)
.subscribe(s -> System.out.println("Received: " + s));
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
前一个代码片段的输出如下:
Received: Alpha
Received: Beta
Received: Gamma
Received: Delta
Received: Epsilon
因为delay()在另一个调度器(如Observable.interval())上操作,我们需要利用sleep()方法来保持应用程序足够长时间,以便看到这个效果。每个排放将被推迟三秒。你可以传递一个可选的第三个布尔参数,表示你是否想推迟错误通知。
对于更复杂的情况,你可以将另一个Observable作为你的delay()参数传递,这将推迟排放,直到那个其他Observable发射一些内容。
注意,还有一个delaySubscription()运算符,它将延迟订阅它前面的Observable,而不是延迟每个单独的排放。
repeat()
repeat()运算符将在onComplete()之后重复上游订阅指定次数。
例如,我们可以通过将2作为repeat()的参数传递给给定的Observable来重复排放两次,如下面的代码片段所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma" ,"Delta", "Epsilon")
.repeat(2)
.subscribe(s -> System.out.println("Received: " + s));
}
}
前一个代码片段的输出如下:
Received: Alpha
Received: Beta
Received: Gamma
Received: Delta
Received: Epsilon
Received: Alpha
Received: Beta
Received: Gamma
Received: Delta
Received: Epsilon
如果你没有指定数字,它将无限重复,在每次onComplete()之后重新订阅。还有一个repeatUntil()运算符,它接受一个布尔供应商 lambda 参数,并将继续重复,直到它产生true。
scan()
scan()方法是一个滚动聚合器。对于每个排放,你将其添加到一个累积中。然后,它将发出每个增量累积。
例如,你可以通过将一个 lambda 表达式传递给scan()方法来发射每个排放的滚动总和,该 lambda 表达式将每个next排放添加到accumulator中:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 3, 7, 10, 2, 14)
.scan((accumulator, next) -> accumulator + next)
.subscribe(s -> System.out.println("Received: " + s));
}
}
前一个代码片段的输出如下:
Received: 5
Received: 8
Received: 15
Received: 25
Received: 27
Received: 41
它发出了初始值5,这是它接收到的第一个值。然后,它接收到了3,并将其加到5上,发出了8。之后,接收到了7,将其加到8上,发出了15,以此类推。这不仅仅可以用于滚动求和。你可以创建许多种类的累积(甚至是非数学的,如字符串连接或布尔缩减)。
注意,scan()与reduce()非常相似,我们将在稍后了解。小心不要混淆这两个。scan()方法为每个发射发出滚动累积,而reduce()在调用onComplete()后产生一个单一的发射,反映最终的累积。scan()可以在无限Observable上安全使用,因为它不需要调用onComplete()。
你也可以为第一个参数提供一个初始值,并将聚合到与发射不同的类型中。如果我们想发出发射的滚动计数,我们可以提供一个初始值0,并为每个发射加1。请注意,初始值将首先发出,所以如果你不想有初始发射,请在scan()之后使用skip(1):
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.scan(0, (total, next) -> total + 1)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上一段代码的输出如下:
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
缩减操作符
你可能会遇到想要将一系列发射合并成一个发射(通常通过Single发出)的时刻。我们将介绍一些完成此任务的运算符。请注意,几乎所有这些运算符都只适用于有限Observable,因为通常我们只能合并有限的数据集。我们将随着介绍这些运算符来探索这种行为。
count()
将发射合并成一个的最简单操作符是count()。它将计算发射的数量,并在调用onComplete()后通过Single发出,如下所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.count()
.subscribe(s -> System.out.println("Received: " + s));
}
}
上一段代码的输出如下:
Received: 5
与大多数缩减操作符一样,这个操作符不应该用在无限Observable上。它将挂起并无限期地工作,永远不会发出计数或调用onComplete()。你应该考虑使用scan()来发出滚动计数。
reduce()
reduce()操作符在语法上与scan()相同,但它只在源调用onComplete()时才发出最终的累积。根据你使用的重载,它可以产生Single或Maybe。如果你想发出所有整数发射的总和,你可以将每个值加到滚动总和中。但只有在最终确定后才会发出:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 3, 7, 10, 2, 14)
.reduce((total, next) -> total + next)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上一段代码的输出如下:
Received: 41
与scan()类似,你可以提供一个种子参数,它将作为累积的初始值。如果我们想将我们的发射转换为单个逗号分隔的值字符串,我们可以像下面这样使用reduce():
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 3, 7, 10, 2, 14)
.reduce("", (total, next) -> total + (total.equals("") ? "" :
",") + next)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上一段代码的输出如下:
Received: 5,3,7,10,2,14
我们提供了一个空字符串作为我们的种子值,并保持一个滚动连接总和,并继续添加到它。我们使用三元运算符来防止前面的逗号,检查total是否是种子值,如果是,则返回一个空字符串而不是逗号。
您的种子值应该是不可变的,例如整数或字符串。如果它是可变的,可能会发生不良副作用,您应该使用collect()(或seedWith())来处理这些情况,我们将在稍后介绍。例如,如果您想将T发射减少到一个集合中,例如List<T>,请使用collect()而不是reduce()。使用reduce()将产生一个不期望的副作用,即每次订阅都使用相同的列表,而不是每次都创建一个新的、空的列表。
all()
all()运算符验证每个发射是否符合指定的条件,并返回一个Single<Boolean>。如果它们都通过,它将发出True。如果它遇到一个失败的,它将立即发出False。在以下代码片段中,我们发出对六个整数的测试,验证它们是否都小于10:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 3, 7, 11, 2, 14)
.all(i -> i < 10)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: false
当遇到all()运算符的11时,它立即发出False并调用onComplete()。它甚至没有到达2或14,因为这将是多余的工作。它已经找到了一个不符合整个测试条件的元素。
如果您在一个空的Observable上调用all(),它将由于空值真原则而发出true。您可以在维基百科上了解更多关于空值真的信息:en.wikipedia.org/wiki/Vacuous_truth。
any()
any()方法将检查至少有一个发射满足特定标准,并返回一个Single<Boolean>。一旦它找到一个符合条件的发射,它将发出true然后调用onComplete()。如果它处理了所有发射并发现它们都是false,它将发出false并调用onComplete()。
在以下代码片段中,我们发出四个日期字符串,将它们转换为LocalDate发射,并测试其中是否有在六月或之后的月份:
import io.reactivex.Observable;
import java.time.LocalDate;
public class Launcher {
public static void main(String[] args) {
Observable.just("2016-01-01", "2016-05-02", "2016-09-12",
"2016-04-03")
.map(LocalDate::parse)
.any(dt -> dt.getMonthValue() >= 6)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: true
当它遇到日期2016-09-12时,它立即发出true并调用onComplete()。它没有继续处理2016-04-03。
如果您在一个空的Observable上调用any(),它将由于空值真原则而发出false。您可以在维基百科上了解更多关于空值真的信息:en.wikipedia.org/wiki/Vacuous_truth。
contains()
contains()运算符将检查一个特定的元素(基于hashCode()/equals()实现)是否曾从Observable中发出。它将返回一个Single<Boolean>,如果找到则发出true,如果没有找到则发出false。
在以下代码片段中,我们发出整数1到10000,并使用contains()检查是否从中发出了数字9563:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10000)
.contains(9563)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: true
如你或许能猜到的,一旦找到元素,它将发出 true 并调用 onComplete() 以及处置操作。如果源调用 onComplete() 但元素未找到,它将发出 false。
集合操作符
集合操作符会将所有发射累积到一个集合,如列表或映射中,然后作为单个发射发出整个集合。集合操作符是减少操作符的另一种形式,因为它们将发射合并成一个。我们将单独介绍它们,因为它们是一个重要的类别。
注意,你应该避免仅仅为了将发射减少到集合中。这可能会削弱反应式编程的好处,在反应式编程中,项目是按顺序、逐个处理的。你只应该在逻辑上将发射以某种方式分组时才将发射合并到集合中。
toList()
一个常见的集合操作符是 toList()。对于一个给定的 Observable<T>,它将收集传入的发射到 List<T> 中,然后将整个 List<T> 作为单个发射(通过 Single<List<T>>)推送出去。在下面的代码片段中,我们将字符串发射收集到 List<String> 中。在先前的 Observable 发出 onComplete() 之后,该列表被推送到 observer 以供打印:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toList()
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: [Alpha, Beta, Gamma, Delta, Epsilon]
默认情况下,toList() 将使用标准的 ArrayList 实现。你可以选择指定一个整数参数作为 capacityHint,这将优化 ArrayList 的初始化,以期望大约有那么多项:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,1000)
.toList(1000)
.subscribe(s -> System.out.println("Received: " + s));
}
}
如果你想要指定除了 ArrayList 之外的不同列表实现,你可以提供一个 Callable lambda 作为参数来构建一个。在下面的代码片段中,我提供了一个 CopyOnWriteArrayList 实例作为我的列表:
import io.reactivex.Observable;
import java.util.concurrent.CopyOnWriteArrayList;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toList(CopyOnWriteArrayList::new)
.subscribe(s -> System.out.println("Received: " + s));
}
}
如果你想要使用 Google Guava 的不可变列表,这会稍微复杂一些,因为它是不可变的并且使用构建器。我们将在本节稍后通过 collect() 展示如何做到这一点。
toSortedList()
toList() 的另一种风味是 toSortedList()。这将收集发射到根据其 Comparator 实现自然排序的列表中。然后,它将排序后的 List<T> 推送到 Observer:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(6, 2, 5, 7, 1, 4, 9, 8, 3)
.toSortedList()
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: [1, 2, 3, 4, 5, 6, 7, 8, 9]
与 sorted() 一样,你可以提供一个 Comparator 作为参数来应用不同的排序逻辑。你还可以指定与 toList() 一样的基础 ArrayList 的初始容量。
toMap() 和 toMultiMap()
对于一个给定的 Observable<T>,toMap() 操作符将收集发射到 Map<K,T> 中,其中 K 是从 lambda Function<T,K> 参数派生的键类型,为每个发射生成键。
如果我们想要将字符串收集到 Map<Char,String> 中,其中每个字符串都根据其第一个字符作为键,我们可以这样做:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toMap(s -> s.charAt(0))
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: {A=Alpha, B=Beta, D=Delta, E=Epsilon, G=Gamma}
s -> s.charAt(0) lambda 参数将每个字符串取出来,并从中导出键以与之配对。在这种情况下,我们正在将字符串的第一个字符作为键。
如果我们想要产生与键关联的不同值,而不是发射值,我们可以提供一个第二个 lambda 参数,该参数将每个发射映射到不同的值。例如,我们可以将每个首字母键映射到该字符串的长度:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toMap(s -> s.charAt(0), String::length)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: {A=5, B=4, D=5, E=7, G=5}
默认情况下,toMap() 将使用 HashMap。您也可以提供一个第三个 lambda 参数,以提供不同的映射实现。例如,我可以提供 ConcurrentHashMap 而不是 HashMap:
import io.reactivex.Observable;
import java.util.concurrent.ConcurrentHashMap;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toMap(s -> s.charAt(0), String::length,
ConcurrentHashMap::new)
.subscribe(s -> System.out.println("Received: " + s));
}
}
注意,如果我有多个发射映射到键,则该键的最后发射将替换后续的发射。如果我将字符串长度作为每个发射的键,则 Alpha 将被 Gamma 替换,而 Gamma 将被 Delta 替换:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toMap(String::length)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: {4=Beta, 5=Delta, 7=Epsilon}
如果您希望给定的键映射到多个发射,则可以使用 toMultiMap(),它将为每个键维护一个相应的值列表。Alpha、Gamma 和 Delta 将被放入一个以长度五为键的列表中:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.toMultimap(String::length)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: {4=[Beta], 5=[Alpha, Gamma, Delta], 7=[Epsilon]}
collect()
当没有收集操作符满足您的需求时,您始终可以使用 collect() 操作符来指定一个不同的类型以收集项目。例如,没有 toSet() 操作符来收集到 Set<T> 中的排放物,但您可以使用 collect() 快速有效地完成此操作。您需要指定两个由 lambda 表达式构建的参数:initialValueSupplier,它将为新的 Observer 提供一个新的 HashSet,以及 collector,它指定了如何将每个发射添加到该 HashSet 中:
import io.reactivex.Observable;
import java.util.HashSet;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.collect(HashSet::new, HashSet::add)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: [Gamma, Delta, Alpha, Epsilon, Beta]
现在的 collect() 操作符将发出一个包含所有发出值的单个 HashSet<String>。
当您需要将排放物放入可变对象中,并且每次都需要一个新的可变对象种子时,请使用 collect() 而不是 reduce()。我们还可以使用 collect() 处理更复杂的情况,这些情况不是简单的集合实现。
假设您已将 Google Guava 作为依赖项添加 (github.com/google/guava),并且您想要将排放物收集到一个 ImmutableList 中。要创建一个 ImmutableList,您必须调用其 builder() 工厂方法以产生一个 ImmutableList.Builder<T>。 然后,您调用其 add() 方法将项目放入构建器中,接着调用 build(),它返回一个密封的、最终的 ImmutableList<T>,该列表不能被修改。
要将发射收集到 ImmutableList 中,你可以为第一个 lambda 参数提供一个 ImmutableList.Builder<T>,然后在第二个参数中通过其 add() 方法添加每个元素。这将在其完全填充后发出 ImmutableList.Builder<T>,然后你可以将其 map() 到其 build() 调用,以发出 ImmutableList<T>:
import com.google.common.collect.ImmutableList;
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.collect(ImmutableList::builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.subscribe(s -> System.out.println("Received: " + s));
}
}
上述代码片段的输出如下:
Received: [Alpha, Beta, Gamma, Delta, Epsilon]
再次强调,collect() 操作符对于将发射收集到任何 RxJava 不直接提供的任意类型非常有用。
错误恢复操作符
在你的 Observable 链中,可能会因为你的操作而出现异常。我们已经知道 onError() 事件是沿着 Observable 链向下传递到 Observer 的。在那之后,订阅终止,不会再有更多的发射发生。但有时,我们想在异常到达 Observer 之前拦截它们,并尝试某种形式的恢复。我们不一定假装错误从未发生并期望发射继续,但我们可以尝试重新订阅或切换到另一个来源的 Observable。
我们仍然可以这样做,只是不是使用 RxJava 操作符,我们很快就会看到。如果你发现错误恢复操作符不符合你的需求,那么很可能会以创造性的方式组合它们。
对于这些示例,让我们将每个整数发射量除以 10,其中一个发射量是 0。这将导致向 Observer 发射一个 "/ by zero" 异常,如下面的代码片段所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上述代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED ERROR: java.lang.ArithmeticException: / by zero
onErrorReturn() 和 onErrorReturnItem()
当你想在发生异常时回退到默认值时,你可以使用 onErrorReturnItem()。如果我们想在发生异常时发出 -1,我们可以这样做:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.onErrorReturnItem(-1)
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上述代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: -1
你也可以提供一个 Function<Throwable,T> 来动态地使用 lambda 生成值。这让你可以访问 Throwable,你可以用它来确定返回的值,如下面的代码片段所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.onErrorReturn(e -> - 1)
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
onErrorReturn() 的放置很重要。如果我们把它放在 map() 操作符之前,错误就不会被捕获,因为错误发生在 onErrorReturn() 之后。为了拦截发出的错误,它必须在错误发生的地方下游。
注意,尽管我们发出了 -1 来处理错误,但序列在那之后仍然终止了。我们没有得到应该跟随的 3、2 或 8。如果你想恢复发射,你只需要在错误可能发生的 map() 操作符内处理错误。你会这样做,而不是使用 onErrorReturn() 或 onErrorReturnItem():
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> {
try {
return 10 / i;
} catch (ArithmeticException e) {
return -1;
}
})
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上述代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: -1
RECEIVED: 3
RECEIVED: 5
RECEIVED: 1
onErrorResumeNext()
与 onErrorReturn() 和 onErrorReturnItem() 类似,onErrorResumeNext() 非常相似。唯一的区别是它接受另一个 Observable 作为参数,在异常发生时发出可能多个值,而不是单个值。
这有点牵强,可能没有实际的应用场景,但我们可以错误发生时发出三个 -1 的发射项:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.onErrorResumeNext(Observable.just(-1).repeat(3))
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上一段代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: -1
RECEIVED: -1
RECEIVED: -1
我们也可以传递它 Observable.empty() 来在发生错误时安静地停止发射,并优雅地调用 onComplete() 函数:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.onErrorResumeNext(Observable.empty())
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上一段代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
与 onErrorReturn() 类似,你可以提供一个 Function<Throwable,Observable<T>> lambda 来从发出的 Throwable 动态生成一个 Observable,如下面的代码片段所示:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.onErrorResumeNext((Throwable e) ->
Observable.just(-1).repeat(3))
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上一段代码的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: -1
RECEIVED: -1
RECEIVED: -1
retry()
尝试恢复的另一种方法是使用 retry() 算子,它有几个参数重载。它将重新订阅前面的 Observable,并希望不再出现错误。
如果你不带参数调用 retry(),它将为每个错误无限次地重新订阅。你需要小心使用 retry(),因为它可能会产生混乱的效果。使用我们的示例将导致它无限次地重复发射这些整数:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.retry()
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上一段代码片段的输出如下:
RECEIVED: 5
RECEIVED: 2
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
...
在放弃并仅向 Observer 发出错误之前指定一个固定的重试次数可能更安全。在下面的代码片段中,我们只会重试两次:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.map(i -> 10 / i)
.retry(2)
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
上一段代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
RECEIVED ERROR: java.lang.ArithmeticException: / by zero
你还可以提供 Predicate<Throwable> 或 BiPredicate<Integer,Throwable> 来有条件地控制何时尝试 retry()。retryUntil() 算子将允许在给定的 BooleanSupplier lambda 为 false 时进行重试。还有一个高级的 retryWhen() 算子,它支持高级的任务组合,例如延迟重试。
行动算子
为了结束这一章,我们将介绍一些有助于调试以及了解 Observable 链的辅助算子。这些是动作或 doOn 算子。
doOnNext(), doOnComplete(), 和 doOnError()
这三个算子:doOnNext()、doOnComplete() 和 doOnError() 就像在 Observable 链的中间放置了一个迷你 Observer。
doOnNext() 算子允许你查看从算子中发出并进入下一个算子的每个发射项。此算子不会以任何方式影响操作或转换发射项。我们只为链中该点的每个事件创建副作用。例如,我们可以在将字符串映射到其长度之前对每个字符串执行操作。在这种情况下,我们只需通过提供一个 Consumer<T> lambda 来打印它们:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.doOnNext(s -> System.out.println("Processing: " + s))
.map(String::length)
.subscribe(i -> System.out.println("Received: " + i));
}
}
上一段代码的输出如下:
Processing: Alpha
Received: 5
Processing: Beta
Received: 4
Processing: Gamma
Received: 5
Processing: Delta
Received: 5
Processing: Epsilon
Received: 7
你还可以利用 doAfterNext(),它在发射传递到下游之后执行操作,而不是在之前。
onComplete() 操作符允许你在 Observable 链中的该点调用 onComplete() 时触发一个操作。这有助于查看 Observable 链的哪些点已经完成,如下面的代码片段所示:**
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.doOnComplete(() -> System.out.println("Source is done
emitting!"))
.map(String::length)
.subscribe(i -> System.out.println("Received: " + i));
}
}
以下代码片段的输出如下:
Received: 5
Received: 4
Received: 5
Received: 5
Received: 7
Source is done emitting!
当然,onError() 会查看正在向上传递的错误,并且你可以用它执行一个操作。这有助于在操作符之间放置,以查看哪个操作符导致了错误:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 2, 4, 0, 3, 2, 8)
.doOnError(e -> System.out.println("Source failed!"))
.map(i -> 10 / i)
.doOnError(e -> System.out.println("Division failed!"))
.subscribe(i -> System.out.println("RECEIVED: " + i),
e -> System.out.println("RECEIVED ERROR: " + e)
);
}
}
以下代码片段的输出如下:
RECEIVED: 2
RECEIVED: 5
RECEIVED: 2
Division failed!
RECEIVED ERROR: java.lang.ArithmeticException: / by zero
我们在两个地方使用了 doOnError() 来查看错误首次出现的位置。由于我们没有看到打印出 Source failed!,而是看到了 Division failed!,我们可以推断错误发生在 map() 操作符中。
使用这三个操作符一起,可以深入了解你的 Observable 操作正在做什么,或者快速创建副作用。
你可以使用 doOnEach() 来指定 onNext()、onComplete() 和 onError() 的所有三个操作。subscribe() 方法接受这三个操作作为 lambda 参数或整个 Observer<T>。这就像在 Observable 链的中间放置了 subscribe()!还有一个 doOnTerminate() 操作符,它在 onComplete() 或 onError() 事件发生或被下游销毁时触发。
doOnSubscribe() 和 doOnDispose()
另外两个有用的操作符是 doOnSubscribe() 和 doOnDispose()。doOnSubscribe() 在 Observable 链中的订阅发生时立即触发一个特定的 Consumer<Disposable>。它提供了对 Disposable 的访问,以便你在该操作中调用 dispose()。doOnDispose() 操作符将在 Observable 链中的该点执行销毁时执行特定操作。
我们使用这两个操作符来打印订阅和销毁发生的时间,如下面的代码片段所示。正如你所预测的,我们首先看到订阅事件被触发。然后,发射通过,最后销毁事件被触发:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.doOnSubscribe(d -> System.out.println("Subscribing!"))
.doOnDispose(() -> System.out.println("Disposing!"))
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
以下代码片段的输出如下:
Subscribing!
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Gamma
RECEIVED: Delta
RECEIVED: Epsilon
Disposing!
注意,doOnDispose() 可能会因为冗余的销毁请求而多次触发,或者如果没有以某种形式销毁,则根本不会触发。另一个选择是使用 doFinally() 操作符,它将在 onComplete() 或 onError() 被调用或由下游销毁后触发。
doOnSuccess()
请记住,Maybe 和 Single 类型没有 onNext() 事件,而是有一个 onSuccess() 操作符来传递单个发射。因此,这两种类型都没有 doOnNext() 操作符,如下面的代码片段所示,而是有一个 doOnSuccess() 操作符。它的使用应该感觉就像 doOnNext():
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just(5, 3, 7, 10, 2, 14)
.reduce((total, next) -> total + next)
.doOnSuccess(i -> System.out.println("Emitting: " + i))
.subscribe(i -> System.out.println("Received: " + i));
}
}
以下代码片段的输出如下:
Emitting: 41
Received: 41
摘要
在本章中,我们涵盖了大量的内容,并且希望到现在为止,你已经开始看到 RxJava 有很多实际的应用。我们介绍了各种抑制和转换发射以及将它们以某种形式减少到单个发射的操作符。你学习了 RxJava 如何通过操作符提供强大的错误恢复方式以及了解Observable链如何操作。
如果你想要了解更多关于 RxJava 操作符的信息,网上有很多资源。Marble 图是 Rx 文档的一种流行形式,可以直观地展示每个操作符的工作方式。rxmarbles.com(rxmarbles.com)是一个流行的、交互式的网络应用,允许你拖动 Marble 发射并查看每个操作符影响的行为。还有一个RxMarbles安卓应用(play.google.com/store/apps/details?id=com.moonfleet.rxmarbles),你可以在你的安卓设备上使用。当然,你还可以在 ReactiveX 网站上看到操作符的完整列表(reactivex.io/documentation/operators.html)。
信不信由你,我们才刚刚开始。本章只涵盖了基本操作符。在接下来的章节中,我们将介绍执行强大行为的操作符,例如并发和多播。但在我们这样做之前,让我们继续介绍那些组合 Observables 的操作符。
第四章:合并 Observables
我们已经介绍了许多抑制、转换、减少和收集发射的操作符。这些操作符可以做很多工作,但关于将多个 Observable 合并并统一它们呢?如果我们想用 ReactiveX 完成更多的工作,我们需要将多个数据流和事件流结合起来,使它们协同工作,并且有操作符和工厂可以实现这一点。这些组合操作符和工厂也可以安全地与在不同线程上发生的 Observables 一起工作(在第六章[4f59db87-4b1d-47e6-95e3-ae0a43193c5f.xhtml],并发与并行化中讨论)。
这是我们开始从使 RxJava 有用转变为使其强大的地方。我们将介绍以下操作来合并 Observables:
-
合并
-
连接
-
不明确
-
压缩
-
组合最新
-
分组
合并
在 ReactiveX 中,一个常见的任务是将两个或多个 Observable<T> 实例合并成一个 Observable<T>。这个合并的 Observable<T> 将会同时订阅其所有合并的源,这使得它对于合并有限和无限 Observable 都非常有效。我们可以通过工厂以及操作符来利用这种合并行为。
Observable.merge() 和 mergeWith()
Observable.merge() 操作符将接受两个或更多发射相同类型 T 的 Observable<T> 源,并将它们合并成一个单一的 Observable<T>。
如果我们只有两个到四个要合并的 Observable<T> 源,你可以将每个源作为参数传递给 Observable.merge() 工厂。在下面的代码片段中,我将两个 Observable<String> 实例合并成了一个 Observable<String>:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
Observable<String> source2 =
Observable.just("Zeta", "Eta", "Theta");
Observable.merge(source1, source2)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
前述程序的输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Gamma
RECEIVED: Delta
RECEIVED: Epsilon
RECEIVED: Zeta
RECEIVED: Eta
RECEIVED: Theta
或者,你可以使用 mergeWith(),它是 Observable.merge() 操作符的版本:
source1.mergeWith(source2)
.subscribe(i -> System.out.println("RECEIVED: " + i));
Observable.merge() 工厂和 mergeWith() 操作符将同时订阅所有指定的源,但如果它们是冷源并且在同一线程上,则可能会按顺序触发发射。这只是一个实现细节,如果你明确想要按顺序触发每个 Observable 的元素并保持它们的发射顺序,应使用 Observable.concat()。
即使看起来顺序被保留了,在使用合并工厂和操作符时也不应依赖于顺序。话虽如此,每个源 Observable 的发射顺序是保持的。源是如何合并的是实现细节,所以如果你想保证顺序,应使用连接工厂和操作符。
如果你有多于四个 Observable<T> 源,你可以使用 Observable.mergeArray() 来传递一个包含你想要合并的 Observable[] 实例的 varargs,如下面的代码片段所示。由于 RxJava 2.0 是为 JDK 6+ 编写的,并且没有访问 @SafeVarargs 注解,你可能会收到一些类型安全警告:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.just("Alpha", "Beta");
Observable<String> source2 =
Observable.just("Gamma", "Delta");
Observable<String> source3 =
Observable.just("Epsilon", "Zeta");
Observable<String> source4 =
Observable.just("Eta", "Theta");
Observable<String> source5 =
Observable.just("Iota", "Kappa");
Observable.mergeArray(source1, source2, source3, source4,
source5)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
前述代码的输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Gamma
RECEIVED: Delta
RECEIVED: Epsilon
RECEIVED: Zeta
RECEIVED: Eta
RECEIVED: Theta
RECEIVED: Iota
RECEIVED: Kappa
您还可以将 Iterable<Observable<T>> 传递给 Observable.merge()。它将合并该 Iterable 中所有的 Observable<T> 实例。我可以通过将这些源放入 List<Observable<T>> 并将它们传递给 Observable.merge() 来以前更安全的方式实现上述示例:
import io.reactivex.Observable;
import java.util.Arrays;
import java.util.List;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.just("Alpha", "Beta");
Observable<String> source2 =
Observable.just("Gamma", "Delta");
Observable<String> source3 =
Observable.just("Epsilon", "Zeta");
Observable<String> source4 =
Observable.just("Eta", "Theta");
Observable<String> source5 =
Observable.just("Iota", "Kappa");
List<Observable<String>> sources =
Arrays.asList(source1, source2, source3, source4, source5);
Observable.merge(sources)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
mergeArray() 获得自己的方法而不是作为 merge() 的重载,原因是为了避免与 Java 8 编译器及其对函数类型的处理产生歧义。这对于所有 xxxArray() 操作符都适用。
Observable.merge() 与无限可观察对象一起工作。由于它将订阅所有可观察对象并在它们可用时立即触发它们的发射,因此您可以将多个无限源合并成一个单一的流。在这里,我们合并了两个分别以一秒和 300 毫秒间隔发射的 Observable.interval() 源。但在合并之前,我们使用发射的索引进行一些数学运算,以确定已经过去的时间,并将其与源名称一起以字符串形式发射。我们让这个过程运行三秒钟:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
//emit every second
Observable<String> source1 = Observable.interval(1,
TimeUnit.SECONDS)
.map(l -> l + 1) // emit elapsed seconds
.map(l -> "Source1: " + l + " seconds");
//emit every 300 milliseconds
Observable<String> source2 =
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> (l + 1) * 300) // emit elapsed milliseconds
.map(l -> "Source2: " + l + " milliseconds");
//merge and subscribe
Observable.merge(source1, source2)
.subscribe(System.out::println);
//keep alive for 3 seconds
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码的输出如下:
Source2: 300 milliseconds
Source2: 600 milliseconds
Source2: 900 milliseconds
Source1: 1 seconds
Source2: 1200 milliseconds
Source2: 1500 milliseconds
Source2: 1800 milliseconds
Source1: 2 seconds
Source2: 2100 milliseconds
Source2: 2400 milliseconds
Source2: 2700 milliseconds
Source1: 3 seconds
Source2: 3000 milliseconds
总结来说,Observable.merge() 将多个发射相同类型 T 的 Observable<T> 源合并并整合成一个单一的 Observable<T>。它适用于无限可观察对象,并且并不保证发射的顺序。如果您关心发射的严格顺序,并且希望每个 Observable 源按顺序触发,那么您可能希望使用 Observable.concat(),我们将在稍后介绍。
flatMap()
在 RxJava 中,flatMap() 是最强大且至关重要的操作符之一。如果您必须投入时间来理解任何 RxJava 操作符,那么这个就是您需要关注的。它是一个执行动态 Observable.merge() 的操作符,通过将每个发射映射到一个 Observable。然后,它将结果可观察对象的发射合并成一个单一的流。
flatMap() 的最简单应用是将 一个发射映射到多个发射。比如说,我们想要发射来自 Observable<String> 的每个字符串中的字符。我们可以使用 flatMap() 来指定一个 Function<T,Observable<R>> lambda,它将每个字符串映射到一个 Observable<String>,这将发射字母。请注意,映射的 Observable<R> 可以发射任何类型的 R,这与源 T 的发射不同。在这个例子中,它恰好是 String,就像源一样:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.flatMap(s -> Observable.fromArray(s.split("")))
.subscribe(System.out::println);
}
}
上述代码的输出如下:
A
l
p
h
a
B
e
t
a
G
a
m
m
...
我们已经将这五个字符串输出映射(通过 flatMap())为每个输出中的字母。我们通过调用每个字符串的 split() 方法来实现这一点,并传递一个空的字符串参数 "",这将根据每个字符进行分隔。这返回一个包含所有字符的 String[] 数组,我们将其传递给 Observable.fromArray() 以输出每个字符。flatMap() 期望每个输出产生一个 Observable,然后它会合并所有生成的 Observable 并以单个流输出它们的值。
这里还有一个例子:让我们取一个 String 值序列(每个值都是一个由 "/" 分隔的值串联),对它们使用 flatMap(),然后在将它们转换为 Integer 输出之前只过滤出数值:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("521934/2342/FOXTROT", "21962/12112/78886
/TANGO",
"283242/4542/WHISKEY/2348562");
source.flatMap(s -> Observable.fromArray(s.split("/")))
.filter(s -> s.matches("[0-9]+")) //use regex to filter
integers
.map(Integer::valueOf)
.subscribe(System.out::println);
}
}
上述代码的输出如下:
521934
2342
21962
12112
78886
283242
4542
2348562
我们通过 / 字符将每个 String 分割,这产生了一个数组。我们将这个数组转换为一个 Observable,并使用 flatMap() 在它上面输出每个 String。我们使用正则表达式 [0-9]+(消除 FOXTROT、TANGO 和 WHISKEY)只过滤出数值字符串,然后将每个输出转换为 Integer。
就像 Observable.merge() 一样,你还可以将输出映射到无限 Observable 并合并它们。例如,我们可以从 Observable<Integer> 中输出简单的 Integer 值,但使用 flatMap() 来驱动 Observable.interval(),其中每个值都作为周期参数。在以下代码片段中,我们输出值 2、3、10 和 7,这将分别产生在 2 秒、3 秒、10 秒和 7 秒输出的间隔 Observable。这四个 Observable 将合并成一个单一的流:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> intervalArguments =
Observable.just(2, 3, 10, 7);
intervalArguments.flatMap(i ->
Observable.interval(i, TimeUnit.SECONDS)
.map(i2 -> i + "s interval: " + ((i + 1) * i) + " seconds
elapsed")
).subscribe(System.out::println);
sleep(12000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码的输出如下:
2s interval: 2 seconds elapsed
3s interval: 3 seconds elapsed
2s interval: 4 seconds elapsed
2s interval: 6 seconds elapsed
3s interval: 6 seconds elapsed
7s interval: 7 seconds elapsed
2s interval: 8 seconds elapsed
3s interval: 9 seconds elapsed
2s interval: 10 seconds elapsed
10s interval: 10 seconds elapsed
2s interval: 12 seconds elapsed
3s interval: 12 seconds elapsed
Observable.merge() 操作符将接受固定数量的 Observable 源。但 flatMap() 会根据每个输入的输出动态地添加新的 Observable 源。这意味着你可以随着时间的推移持续合并新的输入 Observable。
关于 flatMap() 的另一个快速笔记是它可以以许多巧妙的方式使用。时至今日,我仍在不断发现使用它的新方法。但你可以用另一种方式发挥创意,那就是在 flatMap() 中评估每个输出,并确定你想要返回哪种类型的 Observable。例如,如果我的前一个例子向 flatMap() 输出了一个 0,这将破坏生成的 Observable.interval()。但我可以使用一个 if 语句来检查它是否为 0,并返回 Observable.empty(),就像以下代码片段中所示:
Observable<Integer> secondIntervals =
Observable.just(2, 0, 3, 10, 7);
secondIntervals.flatMap(i -> {
if (i == 0)
return Observable.empty();
else
return Observable.interval(i, TimeUnit.SECONDS)
.map(l -> i + "s interval: " + ((l + 1) * i) + " seconds
elapsed");
}).subscribe(System.out::println);
当然,这可能有点过于巧妙,因为你可以在 flatMap() 前面直接使用 filter() 来过滤掉等于 0 的输出。但重点是,你可以在 flatMap() 中评估一个输出,并确定你想要返回哪种 Observable。
flatMap() 也是将热 Observables UI 事件流(如 JavaFX 或 Android 按钮点击)扁平映射到 flatMap() 内部整个过程的绝佳方式。失败和错误恢复可以完全在 flatMap() 内部处理,因此每个流程实例都不会干扰未来的按钮点击。
如果你不想快速按钮点击产生多个冗余的流程实例,你可以使用 doOnNext() 禁用按钮,或者利用 switchMap() 杀死之前的流程,我们将在第七章切换、节流、窗口化和缓冲中讨论。
注意,flatMap() 有许多变体和风味,接受许多重载,为了简洁起见,我们不会深入探讨。我们可以传递一个第二个组合器参数,它是一个 BiFunction<T,U,R> lambda,将最初发射的 T 值与每个扁平映射的 U 值关联,并将两者都转换为 R 值。在我们的早期示例中,我们可以将每个字母与它映射的原始字符串发射关联起来:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.flatMap(s -> Observable.fromArray(s.split("")), (s,r) ->
s + "-" + r)
.subscribe(System.out::println);
}
}
上述代码的输出如下:
Alpha-A
Alpha-l
Alpha-p
Alpha-h
Alpha-a
Beta-B
Beta-e
Beta-t
Beta-a
Gamma-G
...
我们还可以使用 flatMapIterable() 将每个 T 发射映射到 Iterable<R> 而不是 Observable<R>。然后它将为每个 Iterable<R> 发射所有的 R 值,从而节省我们将其转换为 Observable 的步骤和开销。还有将到 Singles (flatMapSingle())、Maybes (flatMapMaybe()) 和 Completables (flatMapCompletable()) 的 flatMap() 变体。许多这些重载也适用于 concatMap(),我们将在下一节中介绍。
连接操作
连接操作与合并操作非常相似,但有一个重要的细微差别:它将按顺序依次发射每个提供的 Observable 的元素,并且按照指定的顺序。它将在当前 Observable 调用 onComplete() 之前不会移动到下一个 Observable。这使得它非常适合确保合并的 Observables 以保证的顺序发射它们的发射。然而,对于无限 Observables 来说,这通常是一个较差的选择,因为无限的 Observable 将无限期地阻塞队列,并永远让后续的 Observables 等待。
我们将介绍用于连接操作的工厂和算子。你会发现它们与合并操作非常相似,只是它们具有顺序行为。
当你想要确保 Observables 按顺序发射它们的发射时,你应该选择连接操作。如果你不关心顺序,则选择合并操作。
Observable.concat() 和 concatWith()
Observable.concat() 工厂是 Observable.merge() 的连接等价物。它将组合多个 Observables 的发射,但将按顺序依次发射每个,并且只有在调用 onComplete() 之后才会移动到下一个。
在以下代码中,我们有两个源可观察对象(Observables)发出字符串。我们可以使用Observable.concat()来触发第一个可观察对象的发射,然后触发第二个可观察对象的发射:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
Observable<String> source2 =
Observable.just("Zeta", "Eta", "Theta");
Observable.concat(source1, source2)
.subscribe(i -> System.out.println("RECEIVED: " + i));
}
}
上述代码的输出如下:
RECEIVED: Alpha
RECEIVED: Beta
RECEIVED: Gamma
RECEIVED: Delta
RECEIVED: Epsilon
RECEIVED: Zeta
RECEIVED: Eta
RECEIVED: Theta
这与之前我们的Observable.merge()示例的输出相同。但如合并部分所述,我们应该使用Observable.concat()来保证发射顺序,因为合并不保证顺序。你还可以使用concatWith()操作符来完成相同的事情,如下面的代码行所示:
source1.concatWith(source2)
.subscribe(i -> System.out.println("RECEIVED: " + i));
如果我们使用Observable.concat()与无限可观察对象,它将永远从遇到的第一个可观察对象发出,并阻止任何后续的可观察对象触发。如果我们想在连接操作中任何地方放置一个无限Observable,它可能被指定为最后一个。这确保了它不会阻止其后的任何可观察对象,因为后面没有可观察对象。我们还可以使用take()操作符将无限可观察对象变为有限。
在这里,我们触发一个每秒发出一次的可观察对象,但只从它那里获取两个发射。之后,它将调用onComplete()并销毁它。然后,一个在它之后连接的可观察对象将永远发出(或在这种情况下,在应用在五秒后退出时)。由于这个第二个可观察对象是Observable.concat()中指定的最后一个,它不会因为无限而阻止任何后续的可观察对象:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
//emit every second, but only take 2 emissions
Observable<String> source1 =
Observable.interval(1, TimeUnit.SECONDS)
.take(2)
.map(l -> l + 1) // emit elapsed seconds
.map(l -> "Source1: " + l + " seconds");
//emit every 300 milliseconds
Observable<String> source2 =
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> (l + 1) * 300) // emit elapsed milliseconds
.map(l -> "Source2: " + l + " milliseconds");
Observable.concat(source1, source2)
.subscribe(i -> System.out.println("RECEIVED: " + i));
//keep application alive for 5 seconds
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码的输出如下:
RECEIVED: Source1: 1 seconds
RECEIVED: Source1: 2 seconds
RECEIVED: Source2: 300 milliseconds
RECEIVED: Source2: 600 milliseconds
RECEIVED: Source2: 900 milliseconds
RECEIVED: Source2: 1200 milliseconds
RECEIVED: Source2: 1500 milliseconds
对于数组和Iterable<Observable<T>>输入,也存在与合并相同的连接对应物。Observable.concatArray()工厂将按顺序在Observable[]数组中触发每个Observable。Observable.concat()工厂也将接受一个Iterable<Observable<T>>,并以相同的方式触发每个Observable<T**>**。
注意,concatMap()有几个变体。当你想要将每个发射映射到Iterable<T>而不是Observable<T>时,使用concatMapIterable()。它将为每个Iterable<T>发出所有的T值,从而节省了将每个值转换为Observable<T>的步骤和开销。还有一个concatMapEager()操作符,它将贪婪地订阅它接收到的所有Observable源,并将发射缓存起来,直到轮到它们发射。
concatMap()
正如存在flatMap(),它动态合并从每个发射派生的可观察对象一样,还有一个称为concatMap()的连接对应物。如果你关心顺序,并且想要每个从每个发射映射的可观察对象在开始下一个之前完成,你应该优先使用此操作符。更具体地说,concatMap()将按顺序合并每个映射的可观察对象,并逐个触发它们。它只有在当前的可观察对象调用onComplete()时才会移动到下一个可观察对象。如果源发射的可观察对象比concatMap()从它们发出要快,那么这些可观察对象将被排队。
我们之前的flatMap()示例如果我们要显式关注发射顺序,将更适合使用concatMap()。尽管我们这里的示例与flatMap()示例具有相同的输出,但当我们显式关注保持顺序并希望按顺序处理每个映射的Observable时,我们应该使用concatMap():
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
source.concatMap(s -> Observable.fromArray(s.split("")))
.subscribe(System.out::println);
}
}
输出结果如下:
A
l
p
h
a
B
e
t
a
G
a
m
m
...
再次强调,您不太可能想要将concatMap()映射到无限Observable。正如您所猜测的,这将导致后续的Observable永远不会触发。您可能会想使用flatMap(),我们将在第六章的并发示例中看到它的使用,并发与并行化。
模糊
在介绍了合并和连接之后,让我们先处理一个简单的组合操作。Observable.amb()工厂(amb代表模糊)将接受一个Iterable<Observable<T>>,并发射第一个发射的Observable的发射,而其他Observable将被销毁。第一个发射的Observable是发射通过的那个。这在您有多个相同数据或事件源且希望最快的一个获胜时很有用。
在这里,我们有两个间隔源,我们使用Observable.amb()工厂将它们组合起来。如果一个每秒发射一次,而另一个每 300 毫秒发射一次,那么后者将会获胜,因为它会先发射:
import io.reactivex.Observable;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
//emit every second
Observable<String> source1 =
Observable.interval(1, TimeUnit.SECONDS)
.take(2)
.map(l -> l + 1) // emit elapsed seconds
.map(l -> "Source1: " + l + " seconds");
//emit every 300 milliseconds
Observable<String> source2 =
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> (l + 1) * 300) // emit elapsed milliseconds
.map(l -> "Source2: " + l + " milliseconds");
//emit Observable that emits first
Observable.amb(Arrays.asList(source1, source2))
.subscribe(i -> System.out.println("RECEIVED: " + i));
//keep application alive for 5 seconds
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
RECEIVED: Source2: 300 milliseconds
RECEIVED: Source2: 600 milliseconds
RECEIVED: Source2: 900 milliseconds
RECEIVED: Source2: 1200 milliseconds
RECEIVED: Source2: 1500 milliseconds
RECEIVED: Source2: 1800 milliseconds
RECEIVED: Source2: 2100 milliseconds
...
您还可以使用ambWith()运算符,它将实现相同的结果:
//emit Observable that emits first
source1.ambWith(source2)
.subscribe(i -> System.out.println("RECEIVED: " + i));
您还可以使用Observable.ambArray()来指定一个varargs数组而不是Iterable<Observable<T>>。
压缩
压缩允许您从每个Observable源中取出发射,并将其组合成一个单一的发射。每个Observable可以发射不同的类型,但您可以将这些不同发射的类型组合成一个单一的发射。以下是一个示例,如果我们有一个Observable<String>和一个Observable<Integer>,我们可以将每个String和Integer配对成一个一对一的配对,并用 lambda 函数连接它们:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
Observable<Integer> source2 = Observable.range(1,6);
Observable.zip(source1, source2, (s,i) -> s + "-" + i)
.subscribe(System.out::println);
}
}
输出结果如下:
Alpha-1
Beta-2
Gamma-3
Delta-4
Epsilon-5
zip()函数接收了Alpha和1,然后将它们配对成一个由破折号-分隔的连接字符串,并将其推向前。然后,它接收Beta和2,并将它们作为连接发射出去,依此类推。一个Observable的发射必须等待与另一个Observable的发射配对。如果一个Observable调用onComplete()而另一个Observable仍有等待配对的发射,那么这些发射将简单地丢弃,因为它们没有可以配对的。这就是为什么6发射发生了,因为我们只有五个字符串发射。
您也可以使用zipWith()运算符来完成此操作,如下所示:
source1.zipWith(source2, (s,i) -> s + "-" + i)
你可以向 Observable.zip() 工厂传递多达九个 Observable 实例。如果你需要更多,你可以传递一个 Iterable<Observable<T>> 或使用 zipArray() 来提供一个 Observable[] 数组。请注意,如果一个或多个源产生的发射速度比另一个快,zip() 将在等待较慢的源提供发射时排队快速发射。这可能会导致不希望的性能问题,因为每个源都会在内存中排队。如果你只关心将每个源的最近一次发射压缩在一起,而不是赶上整个队列,你将想要使用 combineLatest(),我们将在本节稍后介绍。
使用 Observable.zipIterable() 传递一个布尔值 delayError 参数以延迟错误,直到所有源终止,并传递一个整型 bufferSize 以提示每个源期望的元素数量以优化队列大小。你可以指定后者,在某些场景中通过在压缩之前缓冲发射来提高性能。
使用 Observable.interval() 也可以通过压缩来减慢发射。在这里,我们以 1 秒的间隔压缩每个字符串。这将使每个字符串发射延迟一秒,但请注意,五个字符串发射可能会排队等待间隔发射配对:
import io.reactivex.Observable;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<String> strings =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS);
Observable.zip(strings,seconds, (s,l) -> s)
.subscribe(s ->
System.out.println("Received " + s +
" at " + LocalTime.now())
);
sleep(6000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received Alpha at 13:28:28.428
Received Beta at 13:28:29.388
Received Gamma at 13:28:30.389
Received Delta at 13:28:31.389
Received Epsilon at 13:28:32.389
组合最新
Observable.combineLatest() 工厂与 zip() 有一些相似之处,但对于每个源发射的每个发射,它都会立即与每个其他源的最近一次发射耦合。它不会为每个源排队未配对的发射,而是缓存并配对最新的一个。
在这里,让我们使用两个间隔观察者之间的 Observable.combineLatest(),第一个每 300 毫秒发射一次,另一个每秒发射一次:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> source1 =
Observable.interval(300, TimeUnit.MILLISECONDS);
Observable<Long> source2 =
Observable.interval(1, TimeUnit.SECONDS);
Observable.combineLatest(source1, source2,
(l1,l2) -> "SOURCE 1: " + l1 + " SOURCE 2: " + l2)
.subscribe(System.out::println);
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
SOURCE 1: 2 SOURCE 2: 0
SOURCE 1: 3 SOURCE 2: 0
SOURCE 1: 4 SOURCE 2: 0
SOURCE 1: 5 SOURCE 2: 0
SOURCE 1: 5 SOURCE 2: 1
SOURCE 1: 6 SOURCE 2: 1
SOURCE 1: 7 SOURCE 2: 1
SOURCE 1: 8 SOURCE 2: 1
SOURCE 1: 9 SOURCE 2: 1
SOURCE 1: 9 SOURCE 2: 2
这里发生了很多事情,但让我们尝试将其分解。source1 每 300 毫秒发射一次,但前两次发射还没有与每秒发射一次的 source2 中的任何内容配对,并且还没有发生任何发射。最后,经过一秒钟后,source2 推出其第一次发射 0,并与 source1 的最新发射 2(第三次发射)配对。请注意,source1 的前两次发射 0 和 1 被完全遗忘,因为第三次发射 2 现在是最新发射。然后,source1 每 300 毫秒发射 3、4 和 5,但 0 仍然是 source2 的最新发射,所以所有三个都与它配对。然后,source2 发射其第二次发射 1,并与 source2 的最新发射 5 配对。
简单来说,当一个源触发时,它会与来自其他源的最近一次发射耦合。Observable.combineLatest() 在组合 UI 输入时特别有用,因为之前的用户输入通常是不相关的,只有最新的输入才是关注的重点。
withLatestFrom()
与 Observable.combineLatest() 类似,但并不完全相同的是 withLatestfrom() 操作符。它将映射每个 T 发射与其他 Observables 的最新值,并将它们组合起来,但它将只从每个其他 Observables 中取 一个 发射:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> source1 =
Observable.interval(300, TimeUnit.MILLISECONDS);
Observable<Long> source2 =
Observable.interval(1, TimeUnit.SECONDS);
source2.withLatestFrom(source1,
(l1,l2) -> "SOURCE 2: " + l1 + " SOURCE 1: " + l2
) .subscribe(System.out::println);
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
SOURCE 2: 0 SOURCE 1: 2
SOURCE 2: 1 SOURCE 1: 5
SOURCE 2: 2 SOURCE 1: 9
如您所见,source2 每秒发射一次,而 source1 每隔 300 毫秒发射一次。当您在 source2 上调用 withLatestFrom() 并传递 source1 时,它将与 source1 的最新发射结合,但它不会关心任何之前的或随后的发射。
您可以向 withLatestFrom() 传递最多四个任何类型的 Observable 实例。如果您需要更多,您可以传递一个 Iterable<Observable<T>>。
分组
使用 RxJava 可以实现的一个强大操作是将发射按指定的键分组到单独的 Observables 中。这可以通过调用 groupBy() 操作符来实现,它接受一个将每个发射映射到键的 lambda 表达式。然后它将返回一个 Observable<GroupedObservable<K,T>>,它发出一种特殊类型的 Observable,称为 GroupedObservable。GroupedObservable<K,T> 就像任何其他 Observable 一样,但它有一个可访问的键 K 值作为属性。它将发出映射给该特定键的 T 发射。
例如,我们可以使用 groupBy() 操作符来按每个 String 的长度对 Observable<String> 的发射进行分组。我们稍后将订阅它,但这里是如何声明它的:
import io.reactivex.Observable;
import io.reactivex.observables.GroupedObservable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
Observable<GroupedObservable<Integer,String>> byLengths =
source.groupBy(s -> s.length());
}
}
我们可能需要在每个 GroupedObservable 上使用 flatMap(),但在那个 flatMap() 操作中,我们可能希望减少或收集那些具有相同键的发射(因为这将返回一个 Single,我们需要使用 flatMapSingle())。让我们调用 toList() 以便我们可以将发射作为按长度分组的列表发出:
import io.reactivex.Observable;
import io.reactivex.observables.GroupedObservable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
Observable<GroupedObservable<Integer,String>> byLengths =
source.groupBy(s -> s.length());
byLengths.flatMapSingle(grp -> grp.toList())
.subscribe(System.out::println);
}
}
输出如下:
[Beta]
[Alpha, Gamma, Delta]
[Epsilon]
Beta 是唯一长度为四的发射,所以它是该长度键列表中的唯一元素。Alpha、Beta 和 Gamma 都有五个长度,所以它们是从同一个 GroupedObservable 发射的,这些发射是为长度五而发射的,并被收集到同一个列表中。Epsilon 是唯一长度为七的发射,所以它是其列表中的唯一元素。
请记住,GroupedObservable 也有一个 getKey() 方法,它返回与该 GroupedObservable 相关联的键值。如果我们想简单地连接每个 GroupedObservable 的 String 发射,然后将 length 键以这种形式连接起来,我们可以这样做:
import io.reactivex.Observable;
import io.reactivex.observables.GroupedObservable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon");
Observable<GroupedObservable<Integer,String>> byLengths =
source.groupBy(s -> s.length());
byLengths.flatMapSingle(grp ->
grp.reduce("",(x,y) -> x.equals("") ? y : x + ", " + y)
.map(s -> grp.getKey() + ": " + s)
).subscribe(System.out::println);
}
}
输出如下:
4: Beta
5: Alpha, Gamma, Delta
7: Epsilon
仔细注意,GroupedObservables 是一种热和冷 Observable 的奇怪组合。它们之所以不冷,是因为它们不会将未播放的发射重放到第二个 Observer,但它们会缓存发射并将它们刷新到第一个 Observer,确保没有丢失。如果您需要重放发射,可以将它们收集到一个列表中,就像我们之前做的那样,然后对该列表执行操作。您还可以使用缓存操作符,我们将在下一章中学习这些操作符。
摘要
在本章中,我们介绍了以各种有用的方式组合 Observable。合并有助于组合和同时触发多个 Observable,并将它们的发射合并成一个单一的数据流。flatMap() 操作符特别关键,因为动态合并从发射中派生的 Observable 在 RxJava 中打开了大量有用的功能。连接类似于合并,但它按顺序触发源 Observable,而不是一次性触发。与模糊组合一起使用,我们可以选择第一个发射并触发其发射的 Observable。压缩允许您将多个 Observable 的发射组合在一起,而 combineLatest 则在每个源触发时将每个源的最新发射合并在一起。最后,分组允许您将一个 Observable 分割成几个 GroupedObservables,每个 GroupedObservables 都有具有公共键的发射。
抽时间探索组合 Observable 并进行实验,看看它们是如何工作的。它们对于解锁 RxJava 的功能以及快速表达事件和数据转换至关重要。当我们介绍并发性时,我们将在第六章 并发和并行化 中查看一些使用 flatMap() 的强大应用,我们还将介绍如何进行多任务处理和并行化。
第五章:多播、重放和缓存
我们在这本书中已经看到了热和冷Observable的实际应用,尽管我们的大部分例子都是冷Observable(甚至包括使用Observable.interval()的例子)。事实上,在Observable的热和冷特性中有很多细微之处,我们将在本章中探讨。当你有多个Observer时,默认行为是为每个Observer创建一个单独的流。这可能是或可能不是期望的,我们需要意识到何时需要通过多播使用ConnectableObservable来强制Observable变为热。我们在第二章,Observables and Subscribers中简要介绍了ConnectableObservable,但我们将在一个完整的Observable操作符链的更深入背景下探讨它。
在本章中,我们将详细学习使用ConnectableObservable进行多播,并揭示其细微之处。我们还将学习重放和缓存,这两者都进行多播并利用ConnectableObservable。最后,我们将学习 Subjects,这是一个在多播时可能很有用的工具,但应该谨慎使用,仅适用于特定情况。我们将涵盖不同类型的 Subjects,以及何时以及何时不应使用它们。
这里是一个预期的概要:
-
理解多播
-
自动连接
-
重放和缓存
-
Subjects
理解多播
我们在第二章,Observables and Subscribers中之前已经使用过ConnectableObservable。还记得冷Observable,例如Observable.range(),会为每个Observer重新生成发射吗?让我们看一下以下代码:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> threeIntegers = Observable.range(1, 3);
threeIntegers.subscribe(i -> System.out.println("Observer One: " + i));
threeIntegers.subscribe(i -> System.out.println("Observer Two: " + i));
}
}
输出如下:
Observer One: 1
Observer One: 2
Observer One: 3
Observer Two: 1
Observer Two: 2
Observer Two: 3
在这里,Observer One接收了所有三个发射并调用了onComplete()。之后,Observer Two接收了三个发射(这些发射再次被生成)并调用了onComplete()。这些是为两个不同的订阅生成的两个单独的数据流。如果我们想将它们合并成一个数据流,同时将每个发射推送到两个 Observer,我们可以在Observable上调用publish(),这将返回一个ConnectableObservable。我们可以在事先设置好 Observer 后,然后调用connect()来开始发射,这样两个 Observer 将同时接收相同的发射。这将通过以下打印的每个Observer的交错来表示:
import io.reactivex.Observable;
import io.reactivex.observables.ConnectableObservable;
public class Launcher {
public static void main(String[] args) {
ConnectableObservable<Integer> threeIntegers =
Observable.range(1, 3).publish();
threeIntegers.subscribe(i -> System.out.println("Observer One: " + i));
threeIntegers.subscribe(i -> System.out.println("Observer Two: " + i));
threeIntegers.connect();
}
}
输出如下:
Observer One: 1
Observer Two: 1
Observer One: 2
Observer Two: 2
Observer One: 3
Observer Two: 3
使用 ConnectableObservable 将迫使源发射变为热发射,将单个发射流同时推送到所有观察者,而不是为每个 Observer 提供单独的流。这种流合并的想法被称为多播,但其中有一些细微差别,尤其是在操作符介入时。即使你调用 publish() 并使用 ConnectableObservable,任何后续的操作符也可以再次创建单独的流。我们将探讨这种行为以及如何管理它。
使用操作符进行多播
要查看操作符链中的多播是如何工作的,我们将使用 Observable.range() 并将每个发射映射到一个随机整数。由于这些随机值将是非确定性的,并且对于每个订阅都是不同的,这将为我们提供一个很好的方法来查看我们的多播是否工作,因为观察者应该接收到相同的数字。
让我们从发射数字 1 到 3 开始,并将每个数字映射到 0 到 100,000 之间的一个随机整数。如果我们有两个观察者,我们可以预期每个观察者都会得到不同的整数。请注意,由于随机数生成,你的输出将不同于我的输出,只是承认两个观察者都在接收不同的随机整数:
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> threeRandoms = Observable.range(1,3)
.map(i -> randomInt());
threeRandoms.subscribe(i -> System.out.println("Observer 1: " + i));
threeRandoms.subscribe(i -> System.out.println("Observer 2: " + i));
}
public static int randomInt() {
return ThreadLocalRandom.current().nextInt(100000);
}
}
输出如下:
Observer 1: 38895
Observer 1: 36858
Observer 1: 82955
Observer 2: 55957
Observer 2: 47394
Observer 2: 16996
发生在这里的是,Observable.range() 源将产生两个独立的发射生成器,并且每个生成器将为每个 Observer 冷发射一个单独的流。每个流也有它自己的单独的 map() 实例,因此每个 Observer 都得到不同的随机整数。你可以在以下图中直观地看到这两个单独的流的结构:
图 5.1 - 为每个观察者创建了两个独立的操作流
假设你想要向两个观察者发射相同的三个随机整数。你的第一个直觉可能是调用 publish() 在 Observable.range() 之后,以产生一个 ConnectableObservable。然后,你可以在它上面调用 map() 操作符,接着是观察者和一个 connect() 调用。但你将看到这并没有达到我们期望的结果。每个 Observer 仍然得到三个不同的随机整数:
import io.reactivex.Observable;
import io.reactivex.observables.ConnectableObservable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
ConnectableObservable<Integer> threeInts = Observable.range(1,3).publish();
Observable<Integer> threeRandoms = threeInts.map(i -> randomInt());
threeRandoms.subscribe(i -> System.out.println("Observer 1: " + i));
threeRandoms.subscribe(i -> System.out.println("Observer 2: " + i));
threeInts.connect();
}
public static int randomInt() {
return ThreadLocalRandom.current().nextInt(100000);
}
}
输出如下:
Observer 1: 99350
Observer 2: 96343
Observer 1: 4155
Observer 2: 75273
Observer 1: 14280
Observer 2: 97638
这是因为我们在 Observable.range() 之后进行了多播,但多播发生在 map() 操作符之前。尽管我们合并了来自 Observable.range() 的一组发射,但每个观察者仍然会在 map() 时得到一个单独的流。publish() 之前的一切都被合并到一个单独的流中(或者更技术地说,一个单独的代理 Observer)。但在 publish() 之后,它将再次为每个观察者分叉成单独的流,如图所示:

图 5.2 - 在 Observable.range() 之后进行多播将合并间隔发射到一个单独的流,但在 publish() 之后仍将为每个观察者分叉成两个单独的流。
如果我们想要防止map()操作符为每个Observer产生两个独立的数据流,我们需要在map()之后调用publish():
import io.reactivex.Observable;
import io.reactivex.observables.ConnectableObservable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
ConnectableObservable<Integer> threeRandoms = Observable.range(1,3)
.map(i -> randomInt()).publish();
threeRandoms.subscribe(i -> System.out.println("Observer 1: " + i));
threeRandoms.subscribe(i -> System.out.println("Observer 2: " + i));
threeRandoms.connect();
}
public static int randomInt() {
return ThreadLocalRandom.current().nextInt(100000);
}
}
输出如下:
Observer 1: 90125
Observer 2: 90125
Observer 1: 79156
Observer 2: 79156
Observer 1: 76782
Observer 2: 76782
这样更好!每个Observer都得到了相同的三个随机整数,我们有效地在两个Observer之前多播了整个操作,如下面的图所示。现在,由于map()现在在publish()之后,整个链中只有一个流实例:

图 5.3 - 一个完全多播的操作,保证两个Observer都能接收到相同的排放,因为所有操作都在publish()调用之后
何时进行多播
多播有助于防止多个Observer执行冗余工作,并且使所有Observer订阅单个数据流,至少在它们有共同操作这一点上是这样。你可能这样做是为了提高性能,减少内存和 CPU 使用,或者仅仅因为你的业务逻辑需要将相同的排放推送到所有Observer。
数据驱动的冷Observable只有在出于性能原因并且有多个Observer同时接收相同数据时才应该进行多播。记住,多播会创建热ConnectableObservables,你必须小心并适时调用connect(),以确保数据不会被Observer错过。通常在你的 API 中,保持你的冷Observable为冷状态,并在需要将其变为热状态时调用publish()。
即使你的源Observable是热的(例如 JavaFX 或 Android 中的 UI 事件),对该Observable应用操作符也可能导致冗余工作和监听器。当只有一个Observer时(并且多播可能引起不必要的开销),没有必要进行多播。但是如果有多个Observer,你需要找到可以进行多播和整合上游操作的代理点。这个点通常是Observer在上游有共同操作而下游分叉的不同操作的边界。
例如,你可能有一个Observer用于打印随机整数,另一个Observer使用reduce()来计算总和。在这个阶段,实际上这个单一的数据流应该分成两个独立的数据流,因为它们不再冗余,正在执行不同的任务,如下面的代码片段所示:
import io.reactivex.Observable;
import io.reactivex.observables.ConnectableObservable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
ConnectableObservable<Integer> threeRandoms = Observable.range(1,3)
.map(i -> randomInt()).publish();
//Observer 1 - print each random integer
threeRandoms.subscribe(i -> System.out.println("Observer 1: " + i));
//Observer 2 - sum the random integers, then print
threeRandoms.reduce(0, (total,next) -> total + next)
.subscribe(i -> System.out.println("Observer 2: " + i));
threeRandoms.connect();
}
public static int randomInt() {
return ThreadLocalRandom.current().nextInt(100000);
}
}
输出如下:
Observer 1: 40021
Observer 1: 78962
Observer 1: 46146
Observer 2: 165129
这里是一个显示正在多播的常见操作的视觉图:

图 5.4 - 两个Observer之间共享的常见操作放在publish()之后,而分叉操作发生在publish()之后
在对ConnectableObservable和多播有充分理解的基础上,我们将继续探讨一些有助于简化多播的便利操作符。
自动连接
确实有需要手动在ConnectableObservable上调用connect()的时候,以便精确控制发射开始触发的时间。有一些方便的操作符会自动为你调用connect(),但有了这种便利,了解它们的订阅时间行为就很重要了。如果你不小心,允许Observable动态连接可能会适得其反,因为 Observers 可能会错过发射。
autoConnect()
ConnectableObservable上的autoConnect()操作符非常方便。对于给定的ConnectableObservable<T>,调用autoConnect()将返回一个Observable<T>,该Observable<T>将在指定数量的 Observers 订阅后自动调用connect()。由于我们之前的例子有两个 Observers,我们可以在publish()之后立即调用autoConnect(2)来简化流程:
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> threeRandoms = Observable.range(1,3)
.map(i -> randomInt())
.publish()
.autoConnect(2);
//Observer 1 - print each random integer
threeRandoms.subscribe(i -> System.out.println("Observer 1: " + i));
//Observer 2 - sum the random integers, then print
threeRandoms.reduce(0, (total,next) -> total + next)
.subscribe(i -> System.out.println("Observer 2: " + i));
}
public static int randomInt() {
return ThreadLocalRandom.current().nextInt(100000);
}
}
输出如下:
Observer 1: 83428
Observer 1: 77336
Observer 1: 64970
Observer 2: 225734
这避免了我们需要保存ConnectableObservable并稍后调用其connect()方法的问题。相反,它将在获得2个订阅时开始触发,这是我们提前计划和指定的参数。显然,当你有一个未知数量的 Observers 并且你希望他们都能接收到所有发射时,这不会很好地工作。
即使所有下游 Observers 完成或销毁,autoConnect()也会持续其对源的订阅。如果源是有限的并且被销毁,那么当新的Observer在下游订阅时,它将不会再次订阅它。如果我们向我们的例子中添加第三个Observer,但将autoConnect()指定为2而不是3,那么第三个Observer很可能会错过发射:
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> threeRandoms = Observable.range(1,3)
.map(i -> randomInt()).publish().autoConnect(2);
//Observer 1 - print each random integer
threeRandoms.subscribe(i -> System.out.println("Observer 1: " + i));
//Observer 2 - sum the random integers, then print
threeRandoms.reduce(0, (total,next) -> total + next)
.subscribe(i -> System.out.println("Observer 2: " + i));
//Observer 3 - receives nothing
threeRandoms.subscribe(i -> System.out.println("Observer 3: " + i);
}
public static int randomInt() {
return ThreadLocalRandom.current().nextInt(100000);
}
}
输出如下:
Observer 1: 8198
Observer 1: 31718
Observer 1: 97915
Observer 2: 137831
注意,如果你没有为numberOfSubscribers传递任何参数,它将默认为1。如果你希望它在第一次订阅时开始触发,并且不关心后续的 Observers 错过之前的发射,这可能会很有帮助。在这里,我们publish并autoConnect了Observable.interval()。第一个Observer开始触发发射,3 秒后,另一个Observer到来,但错过了最初的几次发射。但从那时起,它确实接收到了实时的发射:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS)
.publish()
.autoConnect();
//Observer 1
seconds.subscribe(i -> System.out.println("Observer 1: " + i));
sleep(3000);
//Observer 2
seconds.subscribe(i -> System.out.println("Observer 2: " + i));
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 2: 3
Observer 1: 4
Observer 2: 4
Observer 1: 5
Observer 2: 5
如果你将autoConnect()函数的numberOfSubscribers参数传递为0,它将立即开始触发,而不等待任何Observers。这可以在不等待任何 Observers 的情况下立即开始触发发射非常有用。
refCount() 和 share()
ConnectableObservable上的refCount()操作符类似于autoConnect(1),它在获得一个订阅后触发。但有一个重要的区别;当它不再有任何 Observers 时,它会销毁自己,并在新的一个到来时重新开始。当它没有更多的 Observers 时,它不会持续对源的订阅,当另一个Observer跟随时,它将基本上“重新开始”。
看这个例子:我们有一个每秒发射一次的 Observable.interval(),它通过 refCount() 进行多播。Observer 1 接收了五个发射,而 Observer 2 接收了两个发射。我们使用 sleep() 函数将它们的订阅错开,它们之间有 3 秒的间隔。由于这两个订阅由于 take() 操作符而有限,它们应该在 Observer 3 来之前终止,并且不应该再有之前的观察者。注意 Observer 3 是从 0 开始的全新间隔重新开始的!让我们看一下以下代码片段:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS)
.publish()
.refCount();
//Observer 1
seconds.take(5)
.subscribe(l -> System.out.println("Observer 1: " + l));
sleep(3000);
//Observer 2
seconds.take(2)
.subscribe(l -> System.out.println("Observer 2: " + l));
sleep(3000);
//there should be no more Observers at this point
//Observer 3
seconds.subscribe(l -> System.out.println("Observer 3: " + l));
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 2: 3
Observer 1: 4
Observer 2: 4
Observer 3: 0
Observer 3: 1
Observer 3: 2
使用 refCount() 可以在多个观察者之间多播,但在没有下游观察者时销毁上游连接。您还可以使用 share() 操作符为 publish().refCount() 创建别名。这将达到相同的结果:
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS).share();
重新播放和缓存
多播还允许我们在多个观察者之间缓存共享的值。这听起来可能有些令人惊讶,但当你仔细思考时,你可能会意识到这是有道理的。如果我们正在多个观察者之间共享数据,那么任何缓存功能也应该在观察者之间共享。重新播放和缓存数据是一种多播活动,我们将探讨如何使用 RxJava 安全且高效地完成它。
重新播放
replay() 操作符是一种强大的方式,可以在一定范围内保留之前的发射,并在新的 Observer 加入时重新发射它们。它将返回一个 ConnectableObservable,它将多播发射以及发射在范围内定义的之前发射。它缓存的之前发射将立即触发新的 Observer 以使其跟上,然后它将从该点开始触发当前发射。
让我们从不带参数的 replay() 开始。这将重新播放所有之前的发射给延迟的观察者,然后一旦延迟的 Observer 跟上,就立即发射当前的发射。如果我们使用 Observable.interval() 每秒发射一次,我们可以在它上面调用 replay() 来多播和重新播放之前的整数发射。由于 replay() 返回 ConnectableObservable,让我们使用 autoConnect() 以便它在第一次订阅时开始发射。3 秒后,我们将引入第二个 Observer。仔细观察发生了什么:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> seconds =
Observable.interval(1, TimeUnit.SECONDS)
.replay()
.autoConnect();
//Observer 1
seconds.subscribe(l -> System.out.println("Observer 1: " + l));
sleep(3000);
//Observer 2
seconds.subscribe(l -> System.out.println("Observer 2: " + l));
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 2: 0
Observer 2: 1
Observer 2: 2
Observer 1: 3
Observer 2: 3
Observer 1: 4
Observer 2: 4
Observer 1: 5
Observer 2: 5
你看到了吗?在 3 秒后,Observer 2 进入并立即接收到了它错过的前三个排放:0、1 和 2。之后,它接收到的排放与 Observer 1 相同。请注意,这可能会因为内存而变得昂贵,因为 replay() 会持续缓存它接收到的所有排放。如果源是无限的,或者你只关心最后的前排放,你可能想指定一个 bufferSize 参数来限制只重放一定数量的最后排放。如果我们对第二个观察者调用 replay(2) 来缓存最后两个排放,它将不会得到 0,但它将接收到 1 和 2。0 落出了那个窗口,并在 2 进入时从缓存中释放。
输出如下:
Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 2: 1
Observer 2: 2
Observer 1: 3
Observer 2: 3
Observer 1: 4
Observer 2: 4
Observer 1: 5
Observer 2: 5
注意,如果你总是想在 replay() 中持久化缓存的值,即使没有订阅,也要与 autoConnect() 一起使用,而不是 refCount()。如果我们通过 Epsilon 字符串发出 Alpha 并使用 replay(1).autoConnect() 来保留最后一个值,我们的第二个 Observer 将只会接收到预期的最后一个值:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma",
"Delta", "Epsilon")
.replay(1)
.autoConnect();
//Observer 1
source.subscribe(l -> System.out.println("Observer 1: " + l));
//Observer 2
source.subscribe(l -> System.out.println("Observer 2: " + l));
}
}
输出如下:
Observer 1: Alpha
Observer 1: Beta
Observer 1: Gamma
Observer 1: Delta
Observer 1: Epsilon
Observer 2: Epsilon
在这里进行修改,使用 refCount() 而不是 autoConnect(),看看会发生什么:
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.replay(1)
.refCount();
输出如下:
Observer 1: Alpha
Observer 1: Beta
Observer 1: Gamma
Observer 1: Delta
Observer 1: Epsilon
Observer 2: Alpha
Observer 2: Beta
Observer 2: Gamma
Observer 2: Delta
Observer 2: Epsilon
这里发生的情况是,refCount() 导致缓存(以及整个链)在 Observer 1 完成时销毁并重置,因为没有更多的观察者。当 Observer 2 进入时,它从头开始,就像它是第一个观察者一样,并构建另一个缓存。这可能不是期望的结果,因此你可能考虑使用 autoConnect() 来持久化 replay() 的状态,而不是在没有观察者时销毁它。
replay() 有其他重载,特别是你可以指定的时间窗口。在这里,我们构建了一个每 300 毫秒发出一次的 Observable.interval() 并订阅它。我们还映射每个发出的连续整数到已过毫秒数。我们将只重放每个新 Observer 的最后 1 秒的排放,我们将在 2 秒后引入它:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> seconds =
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> (l + 1) * 300) // map to elapsed milliseconds
.replay(1, TimeUnit.SECONDS)
.autoConnect();
//Observer 1
seconds.subscribe(l -> System.out.println("Observer 1: " + l));
sleep(2000);
//Observer 2
seconds.subscribe(l -> System.out.println("Observer 2: " + l));
sleep(1000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Observer 1: 300
Observer 1: 600
Observer 1: 900
Observer 1: 1200
Observer 1: 1500
Observer 1: 1800
Observer 2: 1500
Observer 2: 1800
Observer 1: 2100
Observer 2: 2100
Observer 1: 2400
Observer 2: 2400
Observer 1: 2700
Observer 2: 2700
Observer 1: 3000
Observer 2: 3000
仔细观察输出,你会看到当 Observer 2 进入时,它立即接收到了上一秒发生的排放,即 1500 和 1800。在这两个值重放之后,它从那时起接收到的排放与 Observer 1 相同。
你还可以在时间间隔之上指定 bufferSize 参数,这样在那一时间段内只缓冲一定数量的最后排放。如果我们修改我们的示例,只重放最后 1 秒内发生的排放,它应该只重放 1800 给 Observer 2:
Observable<Long> seconds =
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> (l + 1) * 300) // map to elapsed milliseconds
.replay(1, 1, TimeUnit.SECONDS)
.autoConnect();
输出如下:
Observer 1: 300
Observer 1: 600
Observer 1: 900
Observer 1: 1200
Observer 1: 1500
Observer 1: 1800
Observer 2: 1800
Observer 1: 2100
Observer 2: 2100
Observer 1: 2400
Observer 2: 2400
Observer 1: 2700
Observer 2: 2700
Observer 1: 3000
Observer 2: 3000
缓存
当你想要无限期地缓存所有排放物以供长期使用,并且不需要使用 ConnectableObservable 控制对源订阅的行为时,你可以使用 cache() 操作符。它将在第一个下游 Observer 订阅时订阅源并无限期地保留所有值。这使得它不太可能成为无限观察者或可能耗尽你内存的大量数据的候选者:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> cachedRollingTotals =
Observable.just(6, 2, 5, 7, 1, 4, 9, 8, 3)
.scan(0, (total,next) -> total + next)
.cache();
cachedRollingTotals.subscribe(System.out::println);
}
}
你还可以调用 cacheWithInitialCapacity() 并指定缓存中预期的元素数量。这将提前优化该大小元素的缓冲区:
Observable<Integer> cachedRollingTotals =
Observable.just(6, 2, 5, 7, 1, 4, 9, 8, 3)
.scan(0, (total,next) -> total + next)
.cacheWithInitialCapacity(9);
再次强调,除非你真的想无限期地保留所有元素且没有计划在任何时候销毁它们,否则不要使用 cache()。否则,最好使用 replay(),这样你可以更精细地控制缓存大小和窗口以及销毁策略。
Subjects
在我们讨论 Subject 之前,如果不强调它们有使用场景,那就有些疏忽了。初学者经常错误地使用它们,最终陷入复杂的情况。正如你将要学习的,它们既是 Observer 也是 Observable**,充当一个代理多播设备(有点像事件总线)。它们在响应式编程中确实有自己的位置,但在使用它们之前,你应该尽力用完其他选项。ReactiveX 的创造者 Erik Meijer 将它们描述为响应式编程的“可变变量”。就像可变变量有时是必要的,尽管你应该努力追求不可变性一样,Subject 有时是调和命令式范式与响应式范式的一个必要工具。
但在我们讨论何时以及何时不使用它们之前,让我们先看看它们究竟做了什么。
PublishSubject
Subject 有几种实现方式,它是一个抽象类型,实现了 Observable 和 Observer。这意味着你可以在 Subject 上手动调用 onNext()、onComplete() 和 onError(),然后它将把这些事件逐级传递给它的观察者。
最简单的 Subject 类型是 PublishSubject,它像所有 Subject 一样,向其下游观察者热切地广播。其他 Subject 类型增加了更多行为,但 PublishSubject 是“原味”类型,如果你愿意这样称呼的话。
我们可以声明一个 Subject<String>,创建一个映射其长度并订阅它的 Observer,然后调用 onNext() 来传递三个字符串。我们还可以调用 onComplete() 来传达没有更多事件将通过这个 Subject 传递:
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject = PublishSubject.create();
subject.map(String::length)
.subscribe(System.out::println);
subject.onNext("Alpha");
subject.onNext("Beta");
subject.onNext("Gamma");
subject.onComplete();
}
}
输出如下:
5
4
5
这表明 Subject 像神奇的设备一样,可以连接命令式编程和响应式编程,你是对的。接下来,让我们看看何时以及何时不使用 Subject 的案例。
何时使用 Subject
更可能的是,你会使用Subject来积极订阅多个未知数量的源观察者,并将它们的发射合并为一个单一的Observable。由于Subject是一个Observer,你可以轻松地将它们传递给subscribe()方法。这在模块化代码库中可能很有帮助,在这些代码库中,观察者和观察者之间的解耦发生,并且执行Observable.merge()并不那么容易。在这里,我使用Subject来合并和广播两个Observable间隔源:
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.interval(1, TimeUnit.SECONDS)
.map(l -> (l + 1) + " seconds");
Observable<String> source2 =
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> ((l + 1) * 300) + " milliseconds");
Subject<String> subject = PublishSubject.create();
subject.subscribe(System.out::println);
source1.subscribe(subject);
source2.subscribe(subject);
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
300 milliseconds
600 milliseconds
900 milliseconds
1 seconds
1200 milliseconds
1500 milliseconds
1800 milliseconds
2 seconds
2100 milliseconds
2400 milliseconds
2700 milliseconds
3 seconds
3000 milliseconds
当然,我可以用Observable.merge()来完成这个任务(并且技术上我应该这样做)。但是,当你通过依赖注入或其他解耦机制管理模块化代码时,你可能没有提前准备好Observable源来放入Observable.merge()。例如,我可能有一个 JavaFX 应用程序,它有一个来自菜单栏、按钮或按键组合的刷新事件。我可以将这些事件源声明为观察者,并在后端类中将它们订阅到Subject上,以合并事件流,而不需要任何硬耦合。
另一点需要注意的是,第一个在Subject上调用onComplete()的Observable将停止其他Observable推送它们的发射,并且忽略下游的取消请求。这意味着你很可能会使用Subject来处理无限的事件驱动(即用户动作驱动)观察者。话虽如此,我们接下来将探讨Subject容易受到滥用的情况。
当Subject出错时
希望你能感觉到,我们之前关于Subject的例子,发出Alpha、Beta和Gamma,在考虑到我们迄今为止如何架构我们的响应式应用时,可能显得有些反直觉和落后。你这样想是有道理的。我们是在所有观察者都设置完毕之后,才定义了源发射,这个过程不再是从左到右、从上到下读取。由于Subject是活跃的,在观察者设置之前执行onNext()调用会导致这些发射在我们的Subject中被遗漏。如果你像这样移动onNext()调用,你将不会得到任何输出,因为Observer会错过这些发射:
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject = PublishSubject.create();
subject.onNext("Alpha");
subject.onNext("Beta");
subject.onNext("Gamma");
subject.onComplete();
subject.map(String::length)
.subscribe(System.out::println);
}
}
这表明Subject可能有些随意且危险,尤其是如果你将它们暴露给整个代码库,并且任何外部代码都可以调用onNext()来传递发射。例如,假设我们的Subject被暴露给一个外部 API,并且可以任意地将发射Puppy放在Alpha、Beta和Gamma之上。如果我们希望我们的源只发射这些希腊字母,那么它很容易接收到意外的或不受欢迎的发射。响应式编程只有在源观察者来自一个定义良好且可预测的源时才能保持完整性。Subject也不是可丢弃的,因为它们没有公共的dispose()方法,并且当dispose()在下游被调用时,它们不会释放它们的源。
如果你想让这样的数据驱动源保持冷状态,并使用publish()或replay()进行多播以使它们变得热,那就好得多。当你需要使用Subject时,将其转换为Observable或者根本不暴露它。你也可以将一个Subject包裹在某种类的内部,并让方法将事件传递给它。
序列化 Subject
在 Subject 上需要注意的一个关键问题:onSubscribe()、onNext()、onError()和onComplete()调用不是线程安全的!如果你有多个线程调用这四个方法,发射可能会开始重叠并破坏Observable契约,该契约要求发射按顺序发生。如果发生这种情况,一个良好的做法是在Subject上调用toSerialized()以产生一个安全序列化的Subject实现(由私有的SerializedSubject支持)。这将安全地序列化并发事件调用,以确保不会发生灾难性的后果:
Subject<String> subject =
PublishSubject.<String>create().toSerialized();
不幸的是,由于 Java 编译器的限制(包括 Java 8),我们不得不在create()工厂中提前显式声明类型参数String。编译器的类型推断不会超过一个方法调用,所以像之前演示的那样有两个调用将会有编译错误,没有显式类型声明。
BehaviorSubject
还有几种 Subject 的其他风味。除了常用的PublishSubject之外,还有BehaviorSubject。它的行为几乎与PublishSubject相同,但它会将最后发射的项目回放给每个新的下游Observer。这有点像在PublishSubject之后放置replay(1).autoConnect(),但它将这些操作合并为一个单一的优化Subject实现,该实现会积极订阅源:
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject =
BehaviorSubject.create();
subject.subscribe(s -> System.out.println("Observer 1: " + s));
subject.onNext("Alpha");
subject.onNext("Beta");
subject.onNext("Gamma");
subject.subscribe(s -> System.out.println("Observer 2: " + s));
}
}
输出如下:
Observer 1: Alpha
Observer 1: Beta
Observer 1: Gamma
Observer 2: Gamma
在这里,你可以看到Observer 2即使错过了Observer 1接收到的三个发射,仍然接收到了最后的Gamma发射。如果你发现自己需要一个Subject并想要为新观察者缓存最后的发射,你将想要使用一个BehaviorSubject。
ReplaySubject
ReplaySubject类似于PublishSubject后跟一个cache()操作符。它立即捕获发射,无论是否存在下游观察者,并优化在Subject本身内部发生缓存:
import io.reactivex.subjects.ReplaySubject;
import io.reactivex.subjects.Subject;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject =
ReplaySubject.create();
subject.subscribe(s -> System.out.println("Observer 1: " + s));
subject.onNext("Alpha");
subject.onNext("Beta");
subject.onNext("Gamma");
subject.onComplete();
subject.subscribe(s -> System.out.println("Observer 2: " + s));
}
}
输出如下:
Observer 1: Alpha
Observer 1: Beta
Observer 1: Gamma
Observer 2: Alpha
Observer 2: Beta
Observer 2: Gamma
显然,就像使用无参数的replay()或cache()操作符一样,你需要小心使用它,因为如果发射量很大或来源无限,它将缓存所有内容并占用内存。
AsyncSubject
AsyncSubject具有高度定制、有限特定的行为:它将只推送它接收到的最后一个值,然后是一个onComplete()事件:
import io.reactivex.subjects.AsyncSubject;
import io.reactivex.subjects.Subject;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject =
AsyncSubject.create();
subject.subscribe(s ->
System.out.println("Observer 1: " + s),
Throwable::printStackTrace,
() -> System.out.println("Observer 1 done!")
);
subject.onNext("Alpha");
subject.onNext("Beta");
subject.onNext("Gamma");
subject.onComplete();
subject.subscribe(s ->
System.out.println("Observer 2: " + s),
Throwable::printStackTrace,
() -> System.out.println("Observer 2 done!")
);
}
}
输出如下:
Observer 1: Gamma
Observer 1 done!
Observer 2: Gamma
Observer 2 done!
从前面的命令中可以看出,在调用 onComplete() 之前,最后推送到 AsyncSubject 的值是 Gamma。因此,它只向所有 Observer 发射了 Gamma。这种 Subject 你不希望与无限源一起使用,因为它只在调用 onComplete() 时发射。
AsyncSubject 与 Java 8 的 CompletableFuture 类似,它将执行一个你可以选择观察其完成并获取值的计算。你还可以通过在 Observable 上使用 takeLast(1).replay(1) 来模仿 AsyncSubject。在求助于 AsyncSubject 之前,首先尝试使用这种方法。
UnicastSubject
一种有趣且可能有用的 Subject 类型是 UnicastSubject。与所有 Subject 一样,UnicastSubject 将被用来观察和订阅源。但它将缓冲它接收到的所有发射数据,直到有 Observer 订阅它,然后它会将这些发射数据全部释放给 Observer 并清空其缓存:
import io.reactivex.Observable;
import io.reactivex.subjects.ReplaySubject;
import io.reactivex.subjects.Subject;
import io.reactivex.subjects.UnicastSubject;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject =
UnicastSubject.create();
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> ((l + 1) * 300) + " milliseconds")
.subscribe(subject);
sleep(2000);
subject.subscribe(s -> System.out.println("Observer 1: " + s));
sleep(2000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Observer 1: 300 milliseconds
Observer 1: 600 milliseconds
Observer 1: 900 milliseconds
Observer 1: 1200 milliseconds
Observer 1: 1500 milliseconds
Observer 1: 1800 milliseconds
Observer 1: 2100 milliseconds
Observer 1: 2400 milliseconds
Observer 1: 2700 milliseconds
Observer 1: 3000 milliseconds
Observer 1: 3300 milliseconds
Observer 1: 3600 milliseconds
Observer 1: 3900 milliseconds
当你运行此代码时,你将看到在 2 秒后,前六个发射数据在 Observer 订阅时立即释放。然后,它将从那个点开始接收实时发射的数据。但 UnicastSubject 有一个重要的属性;它只与一个 Observer 一起工作,并且对于后续的任何 Observer 都会抛出错误。从逻辑上讲,这是有道理的,因为它设计成在获得 Observer 后释放其内部队列中的缓冲发射数据。但是,当这些缓存的发射数据被释放后,它们不能再次释放给第二个 Observer,因为它们已经消失了。如果你想让第二个 Observer 接收已错过的发射数据,你不妨使用 ReplaySubject。UnicastSubject 的好处是它在获得 Observer 后会清空其缓冲区,从而释放用于该缓冲区的内存。
如果你想要支持多个 Observer 并且只让后续的 Observer 接收实时发射的数据而不接收已错过发射的数据,你可以通过调用 publish() 来创建一个单例 Observer 代理,该代理可以将数据多播给多个 Observer,如下面的代码片段所示:
import io.reactivex.Observable;
import io.reactivex.subjects.Subject;
import io.reactivex.subjects.UnicastSubject;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Subject<String> subject =
UnicastSubject.create();
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(l -> ((l + 1) * 300) + " milliseconds")
.subscribe(subject);
sleep(2000);
//multicast to support multiple Observers
Observable<String> multicast = subject.publish().autoConnect();
//bring in first Observer
multicast.subscribe(s -> System.out.println("Observer 1: " + s));
sleep(2000);
//bring in second Observer
multicast.subscribe(s -> System.out.println("Observer 2: " + s));
sleep(1000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Observer 1: 300 milliseconds
Observer 1: 600 milliseconds
Observer 1: 900 milliseconds
Observer 1: 1200 milliseconds
...
Observer 1: 3900 milliseconds
Observer 1: 4200 milliseconds
Observer 2: 4200 milliseconds
Observer 1: 4500 milliseconds
Observer 2: 4500 milliseconds
摘要
在本章中,我们介绍了使用 ConnectableObservable 和 Subject 进行多播。最大的收获是 Observable 操作符为每个订阅的 Observer 生成单独的事件流。如果你想将这些多个流合并为单个流以避免重复工作,最佳方式是在 Observable 上调用 publish() 以生成 ConnectableObservable。然后你可以手动调用 connect() 来在观察者设置完毕后触发发射,或者使用 autoConnect() 或 refCount() 自动触发连接。
多播还支持回放和缓存,因此迟到的 Observer 可以接收已错过的发射数据。Subject 提供了一种多播和缓存发射数据的方式,但你应该只在现有操作符无法实现你的需求时才使用它们。
在下一章中,我们将开始学习并发编程。这正是 RxJava 真正发光发热的地方,也是响应式编程的卖点之一。
第六章:并发与并行化
在过去 10 年中,对并发的需求迅速增长,并已成为每位专业 Java 程序员的需求。并发(也称为多线程)本质上是一种多任务处理,其中同时执行多个进程。如果你想充分利用硬件的计算能力(无论是手机、服务器、笔记本电脑还是台式计算机),你需要学习如何多线程和利用并发。幸运的是,RxJava 使并发变得更容易且更安全。
在本章中,我们将涵盖以下内容:
-
并发及其必要性的概述
-
subscribeOn() -
observeOn() -
并行化
-
unsubscribeOn()
为什么并发是必要的
在更简单的时代,计算机只有一个 CPU,这使得并发需求变得微不足道。硬件制造商成功找到了让 CPU 更快的方法,这使得单线程程序运行得更快。但最终,这种方法的效果逐渐减弱,制造商发现他们可以通过在设备中放置多个 CPU 来提高计算能力。从台式机和笔记本电脑到服务器和智能手机,现在的多数硬件都配备了多个 CPU 或核心。
对于开发者来说,这是构建软件和编码方式的一次重大颠覆。单线程软件更容易编写,在单核设备上运行良好。但在多核设备上的单线程程序只会使用一个核心,而其他核心则未被利用。如果你想让你的程序可扩展,它需要以利用处理器中所有可用核心的方式编写。
然而,传统上并发并不容易实现。如果你有几个相互独立的进程,那么实现起来更容易。但当资源,尤其是可变对象,在不同线程和进程之间共享时,如果不小心实现锁定和同步,就会引发混乱。不仅线程可能会混乱地相互竞争以读取和更改对象的属性,而且一个线程可能根本看不到另一个线程更改的值!这就是为什么你应该努力使你的对象不可变,并尽可能多地使属性和变量final。这确保了属性和变量是线程安全的,任何可变的东西都应该同步,或者至少使用volatile关键字。
幸运的是,RxJava 使并发和多线程变得更容易且更安全。有方法可以破坏它提供的安全性,但通常,RxJava 主要使用两个操作符:subscribeOn()和observeOn()来为你安全地处理并发。正如我们将在本章中了解到的那样,其他操作符如flatMap()可以与这两个操作符结合使用,以创建强大的并发数据流。
虽然 RxJava 可以帮助你轻松地创建安全且强大的并发应用程序,但了解多线程中的陷阱和陷阱仍然很有帮助。Joshua Bloch 的著名书籍 Effective Java 是每个 Java 开发者都应该拥有的优秀资源,它简洁地涵盖了并发应用程序的最佳实践。如果你想在 Java 并发方面获得深入的知识,确保你也阅读了 Brian Goetz 的 Java Concurrency in Practice。
并发概述
并发,也称为 多线程,可以以多种方式应用。通常,并发的动机是为了更快地完成工作而同时运行多个任务。正如我们在本书的开头所讨论的,并发还可以帮助我们的代码更接近现实世界,在现实世界中,多种活动是同时发生的。
首先,让我们来了解一下并发背后的基本概念。
并发的一个常见应用是同时运行不同的任务。想象一下,你有三项庭院工作:修剪草坪、修剪树木和拔草。如果你自己完成这三项工作,你一次只能做一项。你不能同时修剪草坪和修剪树木。你必须先顺序修剪草坪,然后修剪树木,最后拔草。但是如果你有一个朋友帮忙,你们中的一个可以修剪草坪,而另一个可以修剪树木。第一个完成的人可以接着进行第三项任务:拔草。这样,这三项任务可以更快地完成。
比喻地说,你和你的朋友是 线程。你们一起工作。从整体上看,你们两人是一个 线程池,准备执行任务。家务是线程池中排队的任务,你可以一次执行两个。如果你有更多的线程,你的线程池将拥有更多的带宽来同时处理更多的任务。然而,根据你的计算机有多少核心(以及任务的性质),你只能拥有这么多线程。线程的创建、维护和销毁都很昂贵,随着你创建它们的数量增加,性能的回报会逐渐减少。这就是为什么有一个线程池来 重用 线程并让它们处理任务队列会更好。
理解并行化
并行化(也称为并行性)是一个广泛的概念,可以涵盖上述场景。实际上,你和你的朋友同时执行两个任务,因此是在并行处理。但让我们将并行化应用于同时处理多个相同任务的情况。以一个有 10 名顾客在结账处排队等待的杂货店为例。这 10 名顾客代表了 10 个相同的任务,他们各自需要结账。如果收银员代表一个线程,我们可以有多个收银员来更快地处理这些顾客。但就像线程一样,收银员是昂贵的。我们不希望为每个顾客创建一个收银员,而是集中一定数量的收银员并重复使用它们。如果我们有五个收银员,我们就可以一次处理五个顾客,其余的则在队列中等待。当一个收银员完成一个顾客后,他们可以处理下一个顾客。
这基本上就是并行化所达到的效果。如果你有 1000 个对象,并且需要对每个对象执行昂贵的计算,你可以使用五个线程一次处理五个对象,并可能使这个过程快五倍。关键是要池化这些线程并重复使用它们,因为创建 1000 个线程来处理这 1000 个对象可能会耗尽你的内存并使程序崩溃。
在理解了并发的概念之后,我们将继续讨论在 RxJava 中是如何实现并发的。
引入 RxJava 并发
在 RxJava 中执行并发很简单,但理解起来有些抽象。默认情况下,Observable在立即的线程上执行工作,即声明Observer并订阅它的线程。在我们的许多早期示例中,这是启动我们的main()方法的主线程。
但正如在几个其他示例中暗示的那样,并非所有Observable都会在立即的线程上触发。记得我们使用Observable.interval()的那些时候,如下面的代码所示?让我们看看:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(1, TimeUnit.SECONDS)
.map(i -> i + " Mississippi")
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
0 Mississippi
1 Mississippi
2 Mississippi
3 Mississippi
4 Mississippi
这个Observable实际上会在主线程之外的线程上触发。实际上,主线程将启动Observable.interval(),但不会等待它完成,因为它现在正在自己的单独线程上运行。这实际上使它成为一个并发应用程序,因为它现在正在利用两个线程。如果我们不调用sleep()方法来暂停主线程,它将冲到main()方法的末尾并退出应用程序,而此时间隔还没有机会触发。
通常,并发只有在你有长时间运行或计算密集型过程时才有用。为了帮助我们学习并发而不创建嘈杂的示例,我们将创建一个名为intenseCalculation()的辅助方法来模拟长时间运行的过程。它将简单地接受任何值,然后睡眠 0-3 秒,然后返回相同的值。暂停线程,或暂停它,是模拟忙碌线程工作的好方法:
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
让我们创建两个 Observables,并让两个 Observer 订阅它们。在每个操作中,将每个发射映射到intenseCalculation()方法以减慢它们的速度:
import rx.Observable;
import java.util.concurrent.ThreadLocalRandom;
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.map(s -> intenseCalculation((s)))
.subscribe(System.out::println);
Observable.range(1,6)
.map(s -> intenseCalculation((s)))
.subscribe(System.out::println);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Alpha
Beta
Gamma
Delta
Epsilon
1
2
3
4
5
6
注意,由于每个 Observables 在map()操作中被减慢了 0-3 秒,它们发射发射的速度都很慢。更重要的是,注意第一个发射Alpha、Beta、Gamma的Observable必须首先完成并调用onComplete(),然后才能发射第二个发射数字1到6的Observable。如果我们同时发射两个 Observables 而不是等待一个完成后再开始另一个,我们可以更快地完成这个操作。
我们可以使用subscribeOn()操作符来实现这一点,该操作符建议源在指定的Scheduler上触发发射。在这种情况下,让我们使用Schedulers.computation(),它为计算操作收集了适当数量的线程。它将为每个Observer提供一个线程来推送发射。当调用onComplete()时,线程将被返回给Scheduler以便可以在其他地方重用:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation())
.map(s -> intenseCalculation((s)))
.subscribe(System.out::println);
Observable.range(1,6)
.subscribeOn(Schedulers.computation())
.map(s -> intenseCalculation((s)))
.subscribe(System.out::println);
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下(您的可能不同):
1
2
Alpha
3
4
Beta
5
Gamma
Delta
6
Epsilon
您的输出可能与我不同,因为随机睡眠时间。但请注意,现在两个操作都在同时发射,这使得程序可以更快地完成。而不是主线程被占用,在执行第一个 Observables 的发射之前移动到第二个,它将立即发射两个 Observables 并继续。它不会等待任何一个 Observables 完成。
同时发生多个进程是使应用程序并发的因素。它可以带来更高的效率,因为它将利用更多核心并更快地完成工作。并发还使代码模型更强大,更能代表我们世界的运作方式,在那里多个活动同时发生。
关于 RxJava 令人兴奋的另一个方面是其操作符(至少是官方的操作符和构建良好的自定义操作符)。它们可以在不同的线程上安全地与 Observables 一起工作。甚至像merge()和zip()这样的操作符和工厂,它们可以安全地组合来自不同线程的发射。例如,即使它们在两个不同的计算线程上发射,我们也可以在我们的前一个示例中使用zip():
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation())
.map(s -> intenseCalculation((s)));
Observable<Integer> source2 =
Observable.range(1,6)
.subscribeOn(Schedulers.computation())
.map(s -> intenseCalculation((s)));
Observable.zip(source1, source2, (s,i) -> s + "-" + i)
.subscribe(System.out::println);
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Alpha-1
Beta-2
Gamma-3
Delta-4
Epsilon-5
能够在不同线程上拆分和组合 Observables 是非常强大的,并且消除了回调的痛点。Observables 对它们工作的线程一无所知,这使得并发易于实现、配置和随时演进。
当你开始使反应式应用程序并发时,可能会出现微妙的复杂性。默认情况下,一个非并发应用程序将有一个线程从源到最终的Observer执行所有工作。但是,多个线程可能会导致发射速度超过Observer可以消费的速度(例如,zip()操作符可能有一个源产生发射速度比另一个快)。这可能会使程序过载,内存可能会耗尽,因为某些操作符会缓存积压的发射。当你处理大量发射(超过 10,000)并利用并发时,你可能会想使用 Flowables 而不是 Observables,我们将在第八章中介绍,Flowables 和背压。
保持应用程序存活
到目前为止,我们使用sleep()方法来防止并发反应式应用程序过早退出,仅足够让 Observables 触发。如果你使用 Android、JavaFX 或其他管理自己的非守护线程的框架,这不是一个问题,因为应用程序会为你保持存活。但如果你只是使用main()方法启动程序,并且你想启动长时间运行或无限的 Observables,你可能需要让主线程存活的时间超过 5-20 秒。有时,你可能希望无限期地保持其存活。
让应用程序无限期存活的一种方法是将Long.MAX_VALUE传递给Thread.sleep()方法,如下面的代码所示,其中我们使用Observable.interval()无限期地触发发射:
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(1, TimeUnit.SECONDS)
.map(l -> intenseCalculation((l)))
.subscribe(System.out::println);
sleep(Long.MAX_VALUE);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
好吧,让主线程休眠 9,223,372,036,854,775,807 毫秒不是永远,但这相当于 292,471,208.7 年。对于休眠线程的目的来说,这几乎就是永远!
有方法可以让应用程序仅存活足够长的时间来完成订阅。在 Brian Goetz 的书籍《Java 并发实践》中讨论的经典并发工具,你可以使用CountDownLatch等待两个订阅完成来保持应用程序存活。但更简单的方法是使用 RxJava 中的阻塞操作符。
你可以使用阻塞操作符来停止声明线程并等待发射。通常,阻塞操作符用于单元测试(我们将在第十章中讨论,测试和调试),如果使用不当,它们可能会在生产中吸引反模式。然而,基于有限Observable订阅的生命周期来保持应用程序存活是一个有效的情况来使用阻塞操作符。如图所示,blockingSubscribe()可以用作subscribe()的替代,在允许主线程继续进行并退出应用程序之前停止并等待onComplete()被调用:
import io.reactivex.schedulers.Schedulers;
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation())
.map(Launcher::intenseCalculation)
.blockingSubscribe(System.out::println,
Throwable::printStackTrace,
() -> System.out.println("Done!"));
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Alpha
Beta
Gamma
Delta
Epsilon
Done!
我们将在第十章[ec80132f-c411-4cc1-87b2-7a8ebba089b8.xhtml],测试和调试中更详细地讨论阻塞操作符。在本章的剩余部分,我们将使用subscribeOn()和observeOn()操作符详细探讨并发。但首先,我们将介绍 RxJava 中可用的不同Scheduler类型。
理解调度器
如前所述,线程池是一组线程。根据线程池的策略,线程可能会被持久化和维护,以便可以重用。然后,该线程池会执行任务队列中的任务。
一些线程池保持固定数量的线程(例如我们之前使用的computation()),而其他线程池则会根据需要动态创建和销毁线程。在 Java 中,通常使用ExecutorService作为线程池。然而,RxJava 实现了自己的并发抽象,称为Scheduler。它定义了实际并发提供者(如ExecutorService或 actor 系统)必须遵守的方法和规则。这种结构使 RxJava 在并发源上没有特定的观点。
许多默认的Scheduler实现可以在Schedulers静态工厂类中找到。对于给定的Observer,Scheduler将提供一个线程池中的线程来推送事件。当调用onComplete()时,操作将被处置,并将线程返回到池中,它可能被另一个Observer持久化和重用。
为了使本书实用,我们只会在其自然环境中查看调度器:与subscribeOn()和observeOn()一起使用。如果你想了解更多关于调度器及其独立工作方式的信息,请参阅附录 X 以获取更多信息。
在 RxJava 中,这里有几种调度器类型。在其他库中,如 RxAndroid(在第十一章中介绍,RxJava for Android)和 RxJavaFX(本章后面介绍)也有一些常见的第三方调度器可用。
计算
我们已经看到了计算型Scheduler,你可以通过调用Schedulers.computation()来获取其全局实例。这将根据 Java 会话中可用的处理器数量维护固定数量的线程,这使得它适用于计算任务。计算任务(如数学、算法和复杂逻辑)可能充分利用核心。因此,拥有比可用核心更多的工作线程来执行此类工作没有好处,计算型Scheduler将确保:
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation());
当你不确定将同时执行多少任务,或者只是不确定哪个Scheduler是正确的选择时,默认选择计算型调度器。
许多操作符和工厂默认会使用计算Scheduler,除非您指定不同的一个作为参数。这包括interval()、delay()、timer()、timeout()、buffer()、take()、skip()、takeWhile()、skipWhile()、window()等几个其他操作符的重载。
IO
IO 任务,如读取和写入数据库、网络请求和磁盘存储,在 CPU 上的开销较小,并且通常有等待数据发送或返回的空闲时间。这意味着您可以更自由地创建线程,而Schedulers.io()是适合这种情况的。它将维护与任务数量相等的线程,并根据需要动态增长、缓存和减少线程数量。例如,您可以使用Schedulers.io()来执行使用 RxJava-JDBC 的 SQL 操作(github.com/davidmoten/rxjava-jdbc):
Database db = Database.from(conn);
Observable<String> customerNames =
db.select("SELECT NAME FROM CUSTOMER")
.getAs(String.class)
.subscribeOn(Schedulers.io());
但您必须小心!作为一个经验法则,假设每个订阅都会导致一个新的线程。
New thread
Schedulers.newThread()工厂将返回一个完全不池化线程的Scheduler。它将为每个Observer创建一个新线程,并在完成后销毁该线程。这与Schedulers.io()不同,因为它不会尝试持久化和缓存线程以供重用:
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.newThread());
这可能有助于在您想要立即创建、使用然后销毁线程以避免占用内存的情况下。但对于通常的复杂应用程序,您将希望使用Schedulers.io(),以便尽可能重用线程。您还必须小心,因为Schedulers.newThread()在复杂应用程序中可能会失控(就像Schedulers.io()一样)并创建大量线程,这可能导致您的应用程序崩溃。
Single
当您想在单个线程上顺序运行任务时,可以调用Schedulers.single()。它由适合事件循环的单线程实现支持。它还可以帮助将脆弱的、非线程安全的操作隔离到单个线程:
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.single());
Trampoline
Schedulers.trampoline()是一个有趣的Scheduler。实际上,您不太可能经常调用它,因为它主要用于 RxJava 的内部实现。它的模式也被借用于 UI 调度器,如 RxJavaFX 和 RxAndroid。它就像在立即线程上的默认调度,但它防止了递归调度的情形,即一个任务在同一个线程上调度另一个任务。而不是导致堆栈溢出错误,它将允许当前任务完成,然后执行那个新调度的任务。
ExecutorService
您可以从标准的 Java ExecutorService 构建一个调度器。您可能选择这样做是为了对线程管理策略有更多自定义和精细的控制。例如,假设我们想要创建一个使用 20 个线程的调度器。我们可以创建一个新的固定 ExecutorService,并指定这个数量的线程。然后,您可以通过调用 Schedulers.from() 方法将这个 ExecutorService 包装在一个 Scheduler 实现中:
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Launcher {
public static void main(String[] args) {
int numberOfThreads = 20;
ExecutorService executor =
Executors.newFixedThreadPool(numberOfThreads);
Scheduler scheduler = Schedulers.from(executor);
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(scheduler)
.doFinally(executor::shutdown)
.subscribe(System.out::println);
}
}
ExecutorService 可能会无限期地保持您的程序运行,因此如果您希望其生命周期有限,您必须管理其销毁。如果我只想要支持一个 Observable 订阅的生命周期,我需要调用它的 shutdown() 方法。这就是为什么我在操作通过 doFinally() 操作符终止或销毁后调用其 shutdown() 方法的原因。
启动和关闭调度器
每个默认的 Scheduler 都是在您第一次调用其使用时懒加载实例化的。您可以通过调用它们的 shutdown() 方法来在任何时候销毁 computation()、io()、newThread()、single() 和 trampoline() 调度器,或者通过调用 Schedulers.shutdown() 来销毁所有调度器。这将停止所有线程,并禁止新的任务进入,如果您尝试其他方式,将会抛出错误。您也可以调用它们的 start() 方法,或者 Schedulersers.start(),以重新初始化调度器,以便它们可以再次接受任务。
在桌面和移动应用环境中,您不太可能遇到需要启动和停止调度器的情况。然而,在服务器端,基于 J2EE 的应用程序(例如,Servlets)可能会被卸载和重新加载,并使用不同的类加载器,导致旧的调度器实例泄漏。为了防止这种情况发生,Servlet 应该在其 destroy() 方法中手动关闭 Schedulers。
只有在绝对需要的情况下才管理调度器的生命周期。最好让调度器动态管理它们对资源的使用,并保持它们初始化和可用,以便任务可以在任何时候快速执行。请注意,在关闭调度器之前,最好确保所有未完成的任务都已完成或销毁,否则您可能会留下不一致的状态。
理解 subscribeOn()
我们已经稍微提到了使用 subscribeOn(),但在这个部分,我们将更详细地探讨它,并查看它是如何工作的。
subscribeOn() 操作符会建议上游的 Observable 使用哪个 Scheduler 以及如何在它的某个线程上执行操作。如果那个上游 Observable 没有绑定到特定的 Scheduler,它将使用您指定的 Scheduler。然后,它将使用该线程将发射事件一直推送到最终的 Observer(除非您添加了 observeOn() 调用,我们将在后面介绍)。您可以在 Observable 链中的任何位置放置 subscribeOn(),并且它将建议上游一直到达原始 Observable 使用哪个线程来执行发射。
在以下示例中,你将subscribeOn()放在Observable.just()之后还是放在一个操作符之后,这并没有区别。subscribeOn()将向上游通信到Observable.just(),无论你将其放在哪里,它都将使用哪个Scheduler。为了清晰起见,你应该尽可能将其放置在源附近:
//All three accomplish the same effect with subscribeOn()
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation()) //preferred
.map(String::length)
.filter(i -> i > 5)
.subscribe(System.out::println);
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.map(String::length)
.subscribeOn(Schedulers.computation())
.filter(i -> i > 5)
.subscribe(System.out::println);
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.map(String::length)
.filter(i -> i > 5)
.subscribeOn(Schedulers.computation())
.subscribe(System.out::println);
使用多个Observer来订阅同一个Observable并调用subscribeOn()会导致每个Observer都获得自己的线程(如果没有可用线程,它们将等待可用线程)。在Observer中,你可以通过调用Thread.currentThread().getName()来打印执行线程的名称。我们将通过每次发射来打印这个名称,以证明实际上使用了两个线程来处理这两个Observer:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> lengths =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation())
.map(Launcher::intenseCalculation)
.map(String::length);
lengths.subscribe(i ->
System.out.println("Received " + i + " on thread " +
Thread.currentThread().getName()));
lengths.subscribe(i ->
System.out.println("Received " + i + " on thread " +
Thread.currentThread().getName()));
sleep(10000);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received 5 on thread RxComputationThreadPool-2
Received 4 on thread RxComputationThreadPool-2
Received 5 on thread RxComputationThreadPool-2
Received 5 on thread RxComputationThreadPool-2
Received 5 on thread RxComputationThreadPool-1
Received 7 on thread RxComputationThreadPool-2
Received 4 on thread RxComputationThreadPool-1
Received 5 on thread RxComputationThreadPool-1
Received 5 on thread RxComputationThreadPool-1
注意,一个Observer正在使用名为RxComputationThreadPool-2的线程,而另一个正在使用RxComputationThreadPool-1。这些名称表明它们来自哪个Scheduler(即Computation类型的Scheduler)以及它们的索引。正如这里所示,如果我们只想让一个线程为两个Observer服务,我们可以使用多播操作。只需确保subscribeOn()在多播操作之前:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> lengths =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation())
.map(Launcher::intenseCalculation)
.map(String::length)
.publish()
.autoConnect(2);
lengths.subscribe(i ->
System.out.println("Received " + i + " on thread " +
Thread.currentThread().getName()));
lengths.subscribe(i ->
System.out.println("Received " + i + " on thread " +
Thread.currentThread().getName()));
sleep(10000);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received 5 on thread RxComputationThreadPool-1
Received 5 on thread RxComputationThreadPool-1
Received 4 on thread RxComputationThreadPool-1
Received 4 on thread RxComputationThreadPool-1
Received 5 on thread RxComputationThreadPool-1
Received 5 on thread RxComputationThreadPool-1
Received 5 on thread RxComputationThreadPool-1
大多数Observable工厂,如Observable.fromIterable()和Observable.just(),都会在由subscribeOn()指定的Scheduler上发射项目。对于Observable.fromCallable()和Observable.defer()等工厂,当使用subscribeOn()时,这些源初始化也会在指定的Scheduler上运行。例如,如果你使用Observable.fromCallable()等待 URL 响应,你实际上可以在 IO Scheduler 上执行这项工作,这样主线程就不会阻塞并等待它:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.net.URL;
import java.util.Scanner;
public class Launcher {
public static void main(String[] args) {
Observable.fromCallable(() ->
getResponse("https://api.github.com/users/thomasnield/starred")
).subscribeOn(Schedulers.io())
.subscribe(System.out::println);
sleep(10000);
}
private static String getResponse(String path) {
try {
return new Scanner(new URL(path).openStream(), "UTF-8").useDelimiter("\\A").next();
} catch (Exception e) {
return e.getMessage();
}
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
[{"id":23095928,"name":"RxScala","full_name":"ReactiveX/RxScala","o ....
subscribeOn()的细微差别
重要的是要注意,subscribeOn()在某些源上不会有实际效果(并且会不必要地让工作线程保持待机状态,直到该操作终止)。这可能是因为这些Observable已经使用了特定的Scheduler,如果你想要更改它,你可以提供一个Scheduler作为参数。例如,Observable.interval()将使用Schedulers.computation()并忽略你指定的任何其他subscribeOn()。但你可以提供一个第三个参数来指定要使用不同的Scheduler。在这里,我指定Observable.interval()使用Schedulers.newThread(),如下所示:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(1, TimeUnit.SECONDS, Schedulers.newThread())
.subscribe(i -> System.out.println("Received " + i +
" on thread " + Thread.currentThread().getName()));
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received 0 on thread RxNewThreadScheduler-1
Received 1 on thread RxNewThreadScheduler-1
Received 2 on thread RxNewThreadScheduler-1
Received 3 on thread RxNewThreadScheduler-1
Received 4 on thread RxNewThreadScheduler-1
这也引出了另一个问题:如果你在给定的Observable链上有多个subscribeOn()调用,最上面的一个,或者最接近源的一个,将获胜,并导致后续的调用没有实际效果(除了不必要的资源使用)。如果我使用Schedulers.computation()调用subscribeOn(),然后调用subscribeOn()为Schedulers.io(),那么将使用Schedulers.computation():
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.subscribeOn(Schedulers.computation())
.filter(s -> s.length() == 5)
.subscribeOn(Schedulers.io())
.subscribe(i -> System.out.println("Received " + i +
" on thread " + Thread.currentThread().getName()));
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received Alpha on thread RxComputationThreadPool-1
Received Gamma on thread RxComputationThreadPool-1
Received Delta on thread RxComputationThreadPool-1
这可能发生在 API 返回一个已经通过subscribeOn()预先应用了Scheduler的Observable时,尽管 API 的消费者想要一个不同的Scheduler。因此,API 的设计者被鼓励提供允许参数化使用哪个Scheduler的方法或重载,就像 RxJava 的基于Scheduler的操作符(例如,Observable.interval())一样。
总结来说,subscribeOn()指定了源Observable应该使用的Scheduler,并且它将使用该Scheduler的 worker 将事件推送到最终的Observer。接下来,我们将学习observeOn(),它在Observable链的该点切换到不同的Scheduler。
理解observeOn()
subscribeOn()操作符指示源Observable在哪个Scheduler上发出事件。如果subscribeOn()是Observable链中唯一的并发操作,那么来自该Scheduler的线程将处理整个Observable链,将来自源的事件推送到最终的Observer。然而,observeOn()操作符将在Observable链的该点拦截事件,并将它们切换到不同的Scheduler。
与subscribeOn()不同,observeOn()的位置很重要。它将保留上游的所有操作在默认或subscribeOn()定义的Scheduler上,但将切换到下游的不同Scheduler。在这里,我可以让一个Observable发出一系列由/分隔的字符串,并在 IO Scheduler上拆分它们。但是之后,我可以切换到计算Scheduler来过滤出数字并计算它们的总和,如下面的代码片段所示:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
//Happens on IO Scheduler
Observable.just("WHISKEY/27653/TANGO", "6555/BRAVO", "232352/5675675/FOXTROT")
.subscribeOn(Schedulers.io())
.flatMap(s -> Observable.fromArray(s.split("/")))
//Happens on Computation Scheduler
.observeOn(Schedulers.computation())
.filter(s -> s.matches("[0-9]+"))
.map(Integer::valueOf)
.reduce((total, next) -> total + next)
.subscribe(i -> System.out.println("Received " + i + " on thread "
+ Thread.currentThread().getName()));
sleep(1000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received 5942235 on thread RxComputationThreadPool-1
当然,这个例子计算量不大,在现实生活中,它应该在单个线程上完成。我们引入的并发开销是不必要的,但让我们假装它是一个长时间运行的过程。
再次使用observeOn()来拦截每个事件并将它们推送到不同的Scheduler。在前面的例子中,observeOn()之前的操作符在Scheduler.io()上执行,但之后的操作符由Schedulers.computation()执行。observeOn()之前的上游操作符不受影响,但下游操作符受影响。
你可能会在类似之前模拟的情况中使用observeOn()。如果你想读取一个或多个数据源并等待响应返回,你将想要在Schedulers.io()上执行这部分操作,并且可能会利用subscribeOn(),因为那是初始操作。但是一旦你有了这些数据,你可能想要用它们进行密集的计算,而Scheduler.io()可能不再合适。你将想要将这些操作限制在几个线程上,这些线程将充分利用 CPU。因此,你使用observeOn()将数据重定向到Schedulers.computation()。
实际上,你可以使用多个 observeOn() 操作符来切换 Schedulers 多次。继续我们之前的例子,假设我们想要将计算出的总和写入磁盘并写入一个文件。让我们假装这是一大批数据而不是一个单独的数字,并且我们想要将磁盘写入操作从计算 Scheduler 上移除并放回 IO Scheduler。我们可以通过引入第二个 observeOn() 来实现这一点。让我们还添加一些 doOnNext() 和 doOnSuccess()(由于 Maybe)操作符来查看每个操作使用的线程:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
public class Launcher {
public static void main(String[] args) {
//Happens on IO Scheduler
Observable.just("WHISKEY/27653/TANGO", "6555/BRAVO", "232352/5675675/FOXTROT")
.subscribeOn(Schedulers.io())
.flatMap(s -> Observable.fromArray(s.split("/")))
.doOnNext(s -> System.out.println("Split out " + s + " on thread "
+ Thread.currentThread().getName()))
//Happens on Computation Scheduler
.observeOn(Schedulers.computation())
.filter(s -> s.matches("[0-9]+"))
.map(Integer::valueOf)
.reduce((total, next) -> total + next)
.doOnSuccess(i -> System.out.println("Calculated sum " + i + " on thread "
+ Thread.currentThread().getName()))
//Switch back to IO Scheduler
.observeOn(Schedulers.io())
.map(i -> i.toString())
.doOnSuccess(s -> System.out.println("Writing " + s + " to file on thread "
+ Thread.currentThread().getName()))
.subscribe(s -> write(s,"/home/thomas/Desktop/output.txt"));
sleep(1000);
}
public static void write(String text, String path) {
BufferedWriter writer = null;
try {
//create a temporary file
File file = new File(path);
writer = new BufferedWriter(new FileWriter(file));
writer.append(text);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (Exception e) {
}
}
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Split out WHISKEY on thread RxCachedThreadScheduler-1
Split out 27653 on thread RxCachedThreadScheduler-1
Split out TANGO on thread RxCachedThreadScheduler-1
Split out 6555 on thread RxCachedThreadScheduler-1
Split out BRAVO on thread RxCachedThreadScheduler-1
Split out 232352 on thread RxCachedThreadScheduler-1
Split out 5675675 on thread RxCachedThreadScheduler-1
Split out FOXTROT on thread RxCachedThreadScheduler-1
Calculated sum 5942235 on thread RxComputationThreadPool-1
Writing 5942235 to file on thread RxCachedThreadSchedule
如果你仔细查看输出,你会看到 String 的输出最初是通过线程 RxCachedThreadScheduler-1 在 IO Scheduler 上推送和分割的。之后,每个输出都被切换到计算 Scheduler 并推入一个求和计算中,所有这些都是在线程 RxComputationThreadPool-1 上完成的。然后这个总和被切换到 IO scheduler 以写入一个文本文件(我指定在我的 Linux Mint 桌面上输出),这项工作是在 RxCachedThreadScheduler-1 上完成的(碰巧是推送初始输出的线程,并且被重用了!)。
使用 observeOn() 为 UI 事件线程
当涉及到构建移动应用、桌面应用程序和其他用户体验时,用户对于在操作进行时界面挂起或冻结的界面几乎没有耐心。用户界面的视觉更新通常由一个专门的 UI 线程完成,并且对用户界面的更改必须在那个线程上完成。用户输入事件通常也在 UI 线程上触发。如果用户输入触发了工作,并且这项工作没有被移动到另一个线程,那么这个 UI 线程就会变得繁忙。这就是导致用户界面无响应的原因,而今天的用户期望的要比这个更好。他们希望在后台操作进行时仍然能够与应用程序交互,因此并发性是必不可少的。
幸运的是,RxJava 可以提供帮助!你可以使用 observeOn() 将 UI 事件移动到计算或 IO Scheduler 来执行工作,当结果准备好后,再使用另一个 observeOn() 将其移回 UI 线程。这个 observeOn() 的第二次使用将使用一个围绕 UI 线程的自定义 Scheduler 将输出放在 UI 线程上。RxJava 扩展库,如 RxAndroid (github.com/ReactiveX/RxAndroid)、RxJavaFX (github.com/ReactiveX/RxJavaFX) 和 RxSwing (github.com/ReactiveX/RxSwing),都包含这些自定义 Scheduler 实现。
例如,假设我们有一个简单的 JavaFX 应用程序,每次点击按钮都会显示一个包含 50 个美国州的 ListView<String>。我们可以从按钮创建 Observable<ActionEvent> 并使用 observeOn() 切换到 IO Scheduler(subscribeOn() 对 UI 事件源没有影响)。我们可以在 IO Scheduler 上从文本网络响应中加载 50 个州。一旦返回了州,我们可以使用另一个 observeOn() 将它们放回 JavaFxScheduler,并在 JavaFX UI 线程上安全地将它们填充到 ListView<String> 中:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import io.reactivex.Observable;
import io.reactivex.rxjavafx.observables.JavaFxObservable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
public final class JavaFxApp extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
ListView<String> listView = new ListView<>();
Button refreshButton = new Button("REFRESH");
JavaFxObservable.actionEventsOf(refreshButton)
.observeOn(Schedulers.io())
.flatMapSingle(a ->
Observable.fromArray(getResponse("https://goo.gl/S0xuOi")
.split("\\r?\\n")
).toList()
).observeOn(JavaFxScheduler.platform())
.subscribe(list ->
listView.getItems().setAll(list));
root.getChildren().addAll(listView, refreshButton);
stage.setScene(new Scene(root));
stage.show();
}
private static String getResponse(String path) {
try {
return new Scanner(new URL(path).openStream(), "UTF-8").useDelimiter("\\A").next();
} catch (Exception e) {
return e.getMessage();
}
}
}
以下代码应运行如下所示的 JavaFX 应用程序:

上述截图显示,点击 REFRESH 按钮将发出一个事件,但将其切换到执行检索美国州数据的 IO Scheduler。当响应准备好后,它将发出一个 List<String> 并将其放回 JavaFX Scheduler 以在 ListView 中显示。
这些概念也适用于 Android 开发,你将影响应用程序用户界面的所有操作放在 AndroidSchedulers.mainThread() 而不是 JavaFxScheduler.platform() 上。我们将在第十一章 RxJava for Android 中讨论 Android 开发。
observeOn() 的细微差别
observeOn() 有一些需要注意的细微差别,尤其是在缺乏背压的情况下对性能的影响,我们将在第八章 Flowables and Backpressure 中进行讨论。
假设你有一个包含两套操作的 Observable 链,操作 A 和操作 B。我们暂时不考虑每个操作使用的算子是什么。如果你在这两个操作之间没有 observeOn(),操作将严格按照一个接一个的顺序从源传递到操作 A,然后是操作 B,最后是 Observer。即使有 subscribeOn(),源也不会在当前事件传递到 Observer 之前向下传递下一个事件。
当你引入一个 observeOn() 并将其置于操作 A 和操作 B 之间时,情况会有所不同。发生的情况是,在操作 A 将一个发射事件传递给 observeOn() 之后,它会立即开始下一个发射事件,而不是等待下游完成当前事件,包括操作 B 和 Observer。这意味着源和操作 A 可以比操作 B 和 Observer 更快地产生发射事件。这是一个经典的生产者/消费者场景,其中生产者产生的发射事件速度超过了消费者可以消费的速度。如果出现这种情况,未处理的事件将在 observeOn() 中排队,直到下游能够处理它们。但是,如果你有大量的事件,你可能会遇到内存问题。
这就是为什么当你有 10,000 次或更多次的发射流时,你肯定会想使用Flowable(它支持背压)而不是Observable。背压会向上游通信,直到源处,以减慢速度并一次只产生这么多的发射。即使在引入复杂的并发操作时,它也能恢复基于拉取的发射请求。我们将在第八章中介绍这一点,Flowables 和 Backpressure。
并行化
并行化,也称为并行性或并行计算,是一个广泛的概念,可以用于任何并发活动(包括我们讨论的内容)。但就 RxJava 而言,让我们将其定义为对给定Observable同时处理多个发射。如果我们有一个给定Observable链中的 1000 个发射要处理,如果我们一次处理八个发射而不是一个,我们可能能够更快地完成任务。如果你还记得,Observable契约规定发射必须按序列推送到Observable链中,并且由于并发性永远不会相互竞争。事实上,一次将八个发射推送到Observable链中将是灾难性的,并造成混乱。
这似乎与我们想要达成的目标相矛盾,但幸运的是,RxJava 为你提供了足够的操作符和工具来变得聪明。虽然你不能在同一个Observable上并发推送项目,但你允许同时运行多个Observable,每个Observable都有自己的单独线程推送项目。正如我们在本章中一直所做的那样,我们创建了几个在不同的线程/调度器上运行的Observable,并且甚至将它们组合在一起。实际上,你已经有了这些工具,实现并行化的秘密在于flatMap()操作符,它实际上是一个强大的并发操作符。
在这里,我们有一个Observable发出 10 个整数,并且我们对每个整数执行intenseCalculation()操作。这个过程可能需要一段时间,因为我们是通过sleep()函数模拟的人工处理。让我们在Observer中打印每个整数及其时间,以便我们可以测量性能,如下面的代码所示:
import io.reactivex.Observable;
import java.time.LocalTime;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10)
.map(i -> intenseCalculation(i))
.subscribe(i -> System.out.println("Received " + i + " "
+ LocalTime.now()));
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下(你的输出可能会有所不同):
Received 1 19:11:41.812
Received 2 19:11:44.174
Received 3 19:11:45.588
Received 4 19:11:46.034
Received 5 19:11:47.059
Received 6 19:11:49.569
Received 7 19:11:51.259
Received 8 19:11:54.192
Received 9 19:11:56.196
Received 10 19:11:58.926
随机性当然会导致一些可变性,但在这个例子中,它大约花费了 17 秒来完成(尽管你的时间可能会不同)。如果我们并行处理发射,我们可能会得到更好的性能,那么我们如何做到这一点呢?
记住,序列化(逐个发射项目)只需要在同一个Observable上发生。flatMap()操作符会合并从每个发射中派生出的多个Observable,即使它们是并发的。如果一个灯泡还没有亮起来,请继续阅读。在flatMap()中,让我们将每个发射包装成Observable.just(),使用subscribeOn()在Schedulers.computation()上发射它,然后map它到intenseCalculation()。为了保险起见,让我们在Observer中打印当前线程,如下面的代码所示:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.time.LocalTime;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10)
.flatMap(i -> Observable.just(i)
.subscribeOn(Schedulers.computation())
.map(i2 -> intenseCalculation(i2))
)
.subscribe(i -> System.out.println("Received " + i + " "
+ LocalTime.now() + " on thread "
+ Thread.currentThread().getName()));
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下(你的输出可能会有所不同):
Received 1 19:28:11.163 on thread RxComputationThreadPool-1
Received 7 19:28:11.381 on thread RxComputationThreadPool-7
Received 9 19:28:11.534 on thread RxComputationThreadPool-1
Received 6 19:28:11.603 on thread RxComputationThreadPool-6
Received 8 19:28:11.629 on thread RxComputationThreadPool-8
Received 3 19:28:12.214 on thread RxComputationThreadPool-3
Received 4 19:28:12.961 on thread RxComputationThreadPool-4
Received 5 19:28:13.274 on thread RxComputationThreadPool-5
Received 2 19:28:13.374 on thread RxComputationThreadPool-2
Received 10 19:28:14.335 on thread RxComputationThreadPool-2
这个过程花费了三秒钟完成,你会发现这个处理项目要快得多。当然,我的电脑有八个核心,这就是为什么我的输出可能表明有八个线程在使用。如果你的电脑核心数更少,这个过程将花费更长的时间并使用更少的线程。但它可能仍然比我们之前运行的单一线程实现要快。
我们所做的是为每个发射创建一个Observable,使用subscribeOn()在计算Scheduler上发射它,然后执行intenseCalculation(),这将发生在计算线程之一上。每个实例将从计算Scheduler请求自己的线程,flatMap()将安全地将它们合并回一个序列化的流。
flatMap()一次只会让一个线程出来推送发射,这保持了Observable合同要求发射保持序列化的特性。flatMap()的一个巧妙的行为是,它不会使用过多的同步或阻塞来完成这个任务。如果一个线程已经在将发射从flatMap()推送到Observer下游,任何等待推送发射的线程将简单地让它们的发射由那个正在使用的线程接管。
这里提供的例子不一定是最优的。为每个发射创建一个Observable可能会产生一些不必要的开销。虽然这样做会有更多的组件需要操作,但有一种更简洁的方法来实现并行化。如果我们想避免创建过多的Observable实例,也许我们应该将源Observable分割成固定数量的Observable,使得发射均匀地分配并分布到每一个中。然后,我们可以使用flatMap()来并行化并合并它们。甚至更好,既然我的电脑有八个核心,也许拥有八个Observable来处理八个计算流会是理想的。
我们可以使用 groupBy() 技巧来实现这一点。如果我拥有八个核心,我想将每个发射与 0 到 7 范围内的一个数字相关联。这将产生八个 GroupedObservables,将发射干净地分成八个流。更具体地说,我想循环这八个数字,并将它们作为键分配给每个发射。GroupedObservables 不一定会受到 subscribeOn() 的影响(它将在源线程上发射,除了缓存的发射之外),所以我需要使用 observeOn() 来并行化它们。我还可以使用 io() 或 newThread() 调度程序,因为我已经通过限制 GroupedObservables 的数量来限制了工作者的数量。
下面是我如何做这件事的,但我不是硬编码为八个核心,而是动态查询可用的核心数量:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.time.LocalTime;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
public class Launcher {
public static void main(String[] args) {
int coreCount = Runtime.getRuntime().availableProcessors();
AtomicInteger assigner = new AtomicInteger(0);
Observable.range(1,10)
.groupBy(i -> assigner.incrementAndGet() % coreCount)
.flatMap(grp -> grp.observeOn(Schedulers.io())
.map(i2 -> intenseCalculation(i2))
)
.subscribe(i -> System.out.println("Received " + i + " "
+ LocalTime.now() + " on thread "
+ Thread.currentThread().getName()));
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
下面是输出结果(你的输出将不同):
Received 8 20:27:03.291 on thread RxCachedThreadScheduler-8
Received 6 20:27:03.446 on thread RxCachedThreadScheduler-6
Received 5 20:27:03.495 on thread RxCachedThreadScheduler-5
Received 4 20:27:03.681 on thread RxCachedThreadScheduler-4
Received 7 20:27:03.989 on thread RxCachedThreadScheduler-7
Received 2 20:27:04.797 on thread RxCachedThreadScheduler-2
Received 1 20:27:05.172 on thread RxCachedThreadScheduler-1
Received 9 20:27:05.327 on thread RxCachedThreadScheduler-1
Received 10 20:27:05.913 on thread RxCachedThreadScheduler-2
Received 3 20:27:05.957 on thread RxCachedThreadScheduler-3
对于每个发射,我需要增加它所分组的数字,当它达到 7 时,它将从 0 重新开始。这确保了发射尽可能均匀地分布。我们使用 AtomicInteger 和模运算来实现这一点。如果我们为每个发射递增 AtomicInteger,我们可以将结果除以核心的数量,但返回余数,这始终是一个介于 0 和 7 之间的数字。
AtomicInteger 只是一个被保护在 threadsafe 容器内的整数,并且有方便的 threadsafe 方法,例如 incrementAndGet()。通常,当你有一个对象或状态存在于 Observable 链之外,但被 Observable 链的操作修改(这被称为副作用),那么这个对象应该被做成 threadsafe,尤其是在涉及并发的情况下。你可以在 Brian Goetz 的 Java Concurrency in Practice 中了解更多关于 AtomicInteger 和其他实用工具的信息。
你不必使用处理器数量来控制创建多少个 GroupedObservables。如果你认为更多的工作者会带来更好的性能,你可以指定任何数量。如果你的并发操作是 IO 和计算的混合,并且你发现 IO 更多,你可能从增加允许的线程/GroupedObservables 的数量中受益。
unsubscribeOn()
我们需要讨论的最后一种并发操作符是 unsubscribeOn()。当你销毁一个 Observable 时,这有时可能是一个昂贵的操作,这取决于源的性质。例如,如果你的 Observable 正在使用 RxJava-JDBC 发射数据库查询的结果(github.com/davidmoten/rxjava-jdbc),停止和销毁这个 Observable 可能很昂贵,因为它需要关闭它所使用的 JDBC 资源。
这可能会导致调用 dispose() 的线程变得繁忙,因为它将执行所有停止 Observable 订阅和销毁的操作。如果这是一个 JavaFX 或 Android 中的 UI 线程(例如,因为点击了 CANCEL PROCESSING 按钮),这可能会导致不希望的 UI 冻结,因为 UI 线程正在努力停止和销毁 Observable 操作。
这里有一个简单的每秒发射一次的 Observable。我们暂停主线程三秒钟,然后它会调用 dispose() 来关闭操作。让我们使用 doOnDispose()(将由销毁线程执行)来查看主线程确实正在销毁操作:
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Disposable d = Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(() -> System.out.println("Disposing on thread "
+ Thread.currentThread().getName()))
.subscribe(i -> System.out.println("Received " + i));
sleep(3000);
d.dispose();
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received 0
Received 1
Received 2
Disposing on thread main
让我们添加 unsubscribeOn() 并指定在 Schedulers.io() 上取消订阅。你应该在你想让所有上游操作受到影响的地方放置 unsubscribeOn():
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Disposable d = Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose(() -> System.out.println("Disposing on thread "
+ Thread.currentThread().getName()))
.unsubscribeOn(Schedulers.io())
.subscribe(i -> System.out.println("Received " + i));
sleep(3000);
d.dispose();
sleep(3000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Received 0
Received 1
Received 2
Disposing on thread RxCachedThreadScheduler-1
现在,你会看到销毁是由 IO Scheduler 执行的,其线程由名称 RxCachedThreadScheduler-1 标识。这允许主线程启动销毁并继续,而无需等待其完成。
就像任何并发操作符一样,你实际上不需要为像这个例子这样的轻量级操作使用 unsubscribeOn(),因为它会增加不必要的开销。但是,如果你有资源密集型的 Observable 操作,销毁这些资源又很慢,unsubscribeOn() 可以是一个关键工具,如果调用 dispose() 的线程对高负载敏感。
如果你想针对 Observable 链中的特定部分使用不同的调度器来销毁,你可以使用多个 unsubscribeOn() 调用。直到遇到另一个 unsubscribeOn(),上游的所有内容都将使用那个 Scheduler 销毁。该 unsubscribeOn() 将拥有下一个上游段。
摘要
这可能是我们迄今为止最紧张的一章,但它为你在 RxJava 开发者以及并发大师的专业技能提供了一个转折点!我们涵盖了 RxJava 中可用的不同调度器,以及 RxJavaFX 和 RxAndroid 等其他库中可用的调度器。subscribeOn() 操作符用于向上游的 Observable 链建议推送发射的 Scheduler。observeOn() 将在 Observable 链的该点切换发射到不同的 Scheduler 并在下游使用该 Scheduler。你可以结合使用这两个操作符和 flatMap() 来创建强大的并行化模式,以便充分利用你的多 CPU 能力。我们最后涵盖了 unsubscribeOn(),它帮助我们指定用于销毁操作的不同的 Scheduler,防止在想要保持空闲和可用的线程上出现微妙的挂起,即使它们调用了 dispose() 方法。
需要注意的是,当你开始尝试并发编程时,你需要警惕你现在在线程之间处理的数据量。大量的数据可能会在你的Observable链中排队,有些线程可能会比其他线程更快地产生工作,而其他线程则可能无法及时消费这些工作。当你处理 10,000+个元素时,你将希望使用 Flowables 来防止内存问题,我们将在第八章,“Flowables 和背压”中介绍这一点。
下一章将探讨处理产生排放过快的可观测对象这一主题,并且有一些算子可以在不产生背压的情况下帮助解决这个问题。我们将在下一章中触及这一点。
第七章:切换、节流、窗口化和缓冲
遇到Observable产生排放的速度超过Observer能够消费它们的情况并不少见。这种情况尤其在引入并发性,且Observable链在不同的调度器上运行不同的操作符时发生。无论是某个操作符努力跟上前面的一个,还是最后的Observer努力跟上上游的排放,都可能出现瓶颈,导致排放开始排队在缓慢操作之后。
当然,处理瓶颈的理想方式是利用 Flowable 的背压而不是Observable。Flowable与Observable没有太大区别,除了它通过让Observer以自己的节奏请求排放来告诉源减慢速度,正如我们将在第八章,Flowables 和背压中了解的那样。但并非所有排放源都可以使用背压。你不能指示Observable.interval()(甚至Flowable.interval())减慢速度,因为排放在逻辑上是时间敏感的。要求它减慢速度会使基于时间的排放不准确。用户输入事件,如按钮点击,在逻辑上也不能使用背压,因为你不能通过编程来控制用户。
幸运的是,有一些操作符可以帮助处理快速发射的源,而无需使用背压,并且特别适用于无法使用背压的情况。其中一些操作符将排放批量组合成更易于下游消费的块。其他操作符只是采样排放,忽略其余的。甚至还有一个功能强大的switchMap()操作符,它的工作方式类似于flatMap(),但只会订阅来自最新排放的Observable,并丢弃任何之前的Observable。
我们将在本章中涵盖所有这些主题:
-
缓冲
-
窗口化
-
节流
-
切换
我们将以一个练习结束本章,该练习将按键组合起来以发射用户输入的字符串序列。
缓冲
buffer()操作符将在一定范围内收集排放,并将每个批次作为列表或其他集合类型发射。范围可以通过固定缓冲区大小或一个在间隔处截断的时间窗口来定义,甚至可以通过另一个Observable的排放来切片。
固定大小缓冲
buffer()的最简单重载接受一个count参数,该参数将排放批量组合成固定大小。如果我们想将排放组合成包含八个元素的列表,我们可以这样做:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,50)
.buffer(8)
.subscribe(System.out::println);
}
}
输出如下:
[1, 2, 3, 4, 5, 6, 7, 8]
[9, 10, 11, 12, 13, 14, 15, 16]
[17, 18, 19, 20, 21, 22, 23, 24]
[25, 26, 27, 28, 29, 30, 31, 32]
[33, 34, 35, 36, 37, 38, 39, 40]
[41, 42, 43, 44, 45, 46, 47, 48]
[49, 50]
当然,如果排放的数量不能被干净地除尽,剩余的元素将作为一个最终列表发射,即使它少于指定的数量。这就是为什么前一个代码中的最后一个排放有一个包含两个元素(而不是八个)的列表,只包含49和50。
你还可以提供一个第二个bufferSupplier lambda 参数,将项目放入除了列表之外的另一个集合中,例如HashSet,如下所示(这应该产生相同的输出):
import io.reactivex.Observable;
import java.util.HashSet;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,50)
.buffer(8, HashSet::new)
.subscribe(System.out::println);
}
}
为了使事情更有趣,你也可以提供一个skip参数,该参数指定在开始新的缓冲区之前应该跳过多少项。如果skip等于count,则skip没有效果。但如果它们不同,你可以得到一些有趣的行为。例如,你可以缓冲2个排放,但在下一个缓冲区开始之前跳过3个,如下所示。这将本质上导致每第三个元素不被缓冲:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10)
.buffer(2, 3)
.subscribe(System.out::println);
}
}
输出如下:
[1, 2]
[4, 5]
[7, 8]
[10]
如果你将skip设置为小于count,你可以得到一些有趣的滚动缓冲。如果你将项目缓冲到大小为3,但skip为1,你将得到滚动缓冲。例如,在以下代码中,我们发出数字1到10,但创建缓冲区[1, 2, 3],然后是[2, 3, 4],然后是[3, 4, 5],依此类推:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10)
.buffer(3, 1)
.subscribe(System.out::println);
}
}
输出如下:
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 10]
[9, 10]
[10]
一定要玩转buffer()的skip参数,你可能会发现它的令人惊讶的使用案例。例如,我有时使用buffer(2,1)来一起发出“上一个”排放和下一个排放,如下所示。我还使用filter()来排除最后一个列表,该列表只包含10个元素:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,10)
.buffer(2, 1)
.filter(c -> c.size() == 2)
.subscribe(System.out::println);
}
}
输出如下:
[1, 2]
[2, 3]
[3, 4]
[4, 5]
[5, 6]
[6, 7]
[7, 8]
[8, 9]
[9, 10]
基于时间的缓冲
你可以通过提供一个长TimeUnit在固定时间间隔内使用buffer()。要将排放缓冲到每秒一次的列表中,你可以运行以下代码。请注意,我们正在使源每300毫秒发出一次,由于一秒的间隔截止,每个结果缓冲列表可能包含三个或四个排放:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.buffer(1, TimeUnit.SECONDS)
.subscribe(System.out::println);
sleep(4000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
[300, 600, 900]
[1200, 1500, 1800]
[2100, 2400, 2700]
[3000, 3300, 3600, 3900]
也可以指定一个timeskip参数,它是基于时间的skip的对应参数。它控制每个缓冲开始的时间。
你还可以利用第三个count参数来提供一个最大缓冲区大小。这将导致在每个时间间隔或count达到时发出缓冲排放,以先发生者为准。如果在时间窗口关闭之前count达到,将导致发出一个空的缓冲区。
在这里,我们每秒缓冲一次排放,但将缓冲区大小限制为2:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.buffer(1, TimeUnit.SECONDS, 2)
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
[300, 600]
[900]
[1200, 1500]
[1800]
[2100, 2400]
[2700]
[3000, 3300]
[3600, 3900]
[]
[4200, 4500]
[4800]
注意,基于时间的buffer()操作符将在计算Scheduler上操作。这是有意义的,因为需要在定时器上运行一个单独的线程来执行截止时间。
基于边界的缓冲
buffer()函数最强大的变体是接受另一个Observable作为boundary参数。这个其他Observable发出的类型并不重要。重要的是每次它发出东西时,它都会使用那个排放的时间作为缓冲截止时间。换句话说,另一个Observable排放的任意发生将决定何时“切片”每个缓冲区。
例如,我们可以使用这种技术以每 1 秒的间隔缓冲 300 毫秒的发射。我们可以让 Observable.interval() 的 1 秒作为我们的 Observable.interval() 每 300 毫秒发射的边界:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> cutOffs =
Observable.interval(1, TimeUnit.SECONDS);
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.buffer(cutOffs)
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
[300, 600, 900]
[1200, 1500, 1800]
[2100, 2400, 2700]
[3000, 3300, 3600, 3900]
[4200, 4500, 4800]
这可能是基于高度可变事件缓冲项的最灵活方式。虽然前一个例子中每个切割的时间是一致的(每 1 秒),但 boundary 可以是任何表示任何时间发生的任何事件的 Observable。这种 Observable 作为另一个 Observable 截断的想法是我们将在本章中看到的一个强大的模式。
窗口
window() 操作符几乎与 buffer() 相同,区别在于它们将缓冲操作应用于其他 Observables 而不是集合。这导致了一个 Observable<Observable<T>>,它发射 Observables。每个 Observable 的发射都会为每个作用域缓存发射,然后在订阅后一次性清除(类似于我们在 第四章,组合 Observables)中使用的 GroupedObservable)。这允许在可用时立即处理发射,而不是等待每个列表或集合最终确定并发射。如果你想要使用操作符转换每个批次,window() 操作符也非常方便。
就像 buffer() 一样,你可以使用固定大小、时间间隔或来自另一个 Observable 的边界来截断每个批次。
固定大小窗口
让我们修改之前的例子,其中我们将 50 个整数缓冲到大小为 8 的列表中,但我们将使用 window() 来将它们作为 Observables 缓冲。我们可以反应性地将每个批次转换成除了集合之外的其他东西,例如使用管道 "|" 分隔符将发射连接成字符串:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,50)
.window(8)
.flatMapSingle(obs -> obs.reduce("", (total, next) -> total
+ (total.equals("") ? "" : "|") + next))
.subscribe(System.out::println);
}
}
输出如下:
1|2|3|4|5|6|7|8
9|10|11|12|13|14|15|16
17|18|19|20|21|22|23|24
25|26|27|28|29|30|31|32
33|34|35|36|37|38|39|40
41|42|43|44|45|46|47|48
49|50
就像 buffer() 一样,你也可以提供一个 skip 参数。这是在开始新窗口之前需要跳过的发射数量。这里,我们的窗口大小是 2,但我们跳过了三个项目。然后我们取每个窗口 Observables 并将其减少到字符串连接:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1,50)
.window(2, 3)
.flatMapSingle(obs -> obs.reduce("", (total, next) -> total
+ (total.equals("") ? "" : "|") + next))
.subscribe(System.out::println);
}
}
输出如下:
1|2
4|5
7|8
10|11
13|14
16|17
19|20
22|23
25|26
28|29
31|32
34|35
37|38
40|41
43|44
46|47
49|50
基于时间的窗口
如你可能猜到的,你可以像 buffer() 一样在时间间隔内截断窗口 Observables。这里,我们有一个每 300 毫秒发射一次的 Observable,我们每 1 秒将其切割成单独的 Observables。然后我们将对每个 Observable 使用 flatMapSingle() 来进行发射的字符串连接:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.window(1, TimeUnit.SECONDS)
.flatMapSingle(obs -> obs.reduce("", (total, next) -> total
+ (total.equals("") ? "" : "|") + next))
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
300|600|900
1200|1500|1800
2100|2400|2700
3000|3300|3600|3900
4200|4500|4800
当然,你可以使用这些生成的 Observables 进行除了字符串连接之外的转换。你可以使用我们到目前为止学到的所有操作符对每个窗口 Observables 执行不同的操作,你很可能会在 flatMap()、concatMap() 或 switchMap() 中完成这项工作。
使用基于时间的window()运算符,您也可以指定count或timeshift参数,就像它的buffer()对应物一样。
基于边界的窗口化
可能不会令人惊讶,因为window()与buffer()类似(除了它发射 Observables 而不是连接),您也可以使用另一个Observable作为boundary。
在这里,我们使用每秒发射一次的Observable.interval()作为每 300 毫秒发射一次的Observable的boundary。我们利用每个发射的Observable将发射项连接成连接字符串:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<Long> cutOffs =
Observable.interval(1, TimeUnit.SECONDS);
Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.window(cutOffs)
.flatMapSingle(obs -> obs.reduce("", (total, next) -> total
+ (total.equals("") ? "" : "|") + next))
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
300|600|900
1200|1500|1800
2100|2400|2700
3000|3300|3600|3900
4200|4500|4800
再次强调,使用另一个Observable作为boundary的好处是,它允许您使用任何Observable的任意发射时间来切断每个窗口,无论是按钮点击、网络请求还是任何其他事件。这使得它在涉及可变性时成为切片window()或buffer()操作的最灵活方式。
节流
buffer()和window()运算符根据定义的范围将发射项批量收集到集合或 Observables 中,它们会定期合并而不是省略发射项。然而,throttle()运算符在发射项快速发生时省略发射项。这在快速发射被认为是冗余或不需要时很有用,例如用户反复点击按钮。对于这些情况,您可以使用throttleLast()、throttleFirst()和throttleWithTimeout()运算符,只让快速发射序列中的第一个或最后一个元素通过。您选择众多快速发射中的哪一个取决于您选择的运算符、参数和参数。
对于本节中的示例,我们将处理以下情况:我们有三个Observable.interval()源,第一个每 100 毫秒发射一次,第二个每 300 毫秒发射一次,第三个每 2000 毫秒发射一次。我们从第一个源中只取 10 个发射项,从第二个源中取 3 个,从第三个源中取 2 个。正如您所看到的,我们将使用Observable.concat()将它们一起使用,以创建一个在不同间隔下改变节奏的快速序列:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 = Observable.interval(100, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 100) // map to elapsed time
.map(i -> "SOURCE 1: " + i)
.take(10);
Observable<String> source2 = Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.map(i -> "SOURCE 2: " + i)
.take(3);
Observable<String> source3 = Observable.interval(2000, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 2000) // map to elapsed time
.map(i -> "SOURCE 3: " + i)
.take(2);
Observable.concat(source1, source2, source3)
.subscribe(System.out::println);
sleep(6000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
SOURCE 1: 100
SOURCE 1: 200
SOURCE 1: 300
SOURCE 1: 400
SOURCE 1: 500
SOURCE 1: 600
SOURCE 1: 700
SOURCE 1: 800
SOURCE 1: 900
SOURCE 1: 1000
SOURCE 2: 300
SOURCE 2: 600
SOURCE 2: 900
SOURCE 3: 2000
SOURCE 3: 4000
第一个源在 1 秒内快速推送 10 个发射项,第二个在 1 秒内推送 3 个,第三个在 4 秒内推送 2 个。让我们使用一些throttle()运算符来只选择其中的一些发射项,忽略其余的。
throttleLast() / sample()
throttleLast()运算符(别名sample())将仅在固定时间间隔内发出最后一个项目。修改您之前的示例,以每秒 1 次的时间间隔使用throttleLast(),如下所示:
Observable.concat(source1, source2, source3)
.throttleLast(1, TimeUnit.SECONDS)
.subscribe(System.out::println);
输出如下:
SOURCE 1: 900
SOURCE 2: 900
SOURCE 3: 2000
如果您研究输出,您可以看到,每个 1 秒间隔的最后一个发射项是唯一通过的那个。这实际上通过定时器深入流中并拉出最新的一个来有效地采样发射项。
如果你想在较长的时间间隔内更自由地节流,你会得到更少的发射,因为这实际上减少了采样频率。这里,我们每两秒使用一次 throttleLast():
Observable.concat(source1, source2, source3)
.throttleLast(2, TimeUnit.SECONDS)
.subscribe(System.out::println);
输出如下:
SOURCE 2: 900
SOURCE 3: 2000
如果你想在较短的时间间隔内更积极地节流,你会得到更多的发射,因为这样可以增加采样频率。这里,我们每 500 毫秒使用一次 throttleLast():
Observable.concat(source1, source2, source3)
.throttleLast(500, TimeUnit.MILLISECONDS)
.subscribe(System.out::println);
输出如下:
SOURCE 1: 400
SOURCE 1: 900
SOURCE 2: 300
SOURCE 2: 900
SOURCE 3: 2000
再次,throttleLast() 将在每个固定的时间间隔推进最后一个发射。接下来,我们将介绍 throttleFirst(),它将发出第一个项目。
throttleFirst()
throttleFirst() 几乎与 throttleLast() 操作相同,但它会在每个固定的时间间隔发出发生的第一个项目。如果我们修改我们的例子为每 1 秒使用一次 throttleFirst(),我们应该得到如下输出:
Observable.concat(source1, source2, source3)
.throttleFirst(1, TimeUnit.SECONDS)
.subscribe(System.out::println);
输出如下:
SOURCE 1: 100
SOURCE 2: 300
SOURCE 3: 2000
SOURCE 3: 4000
实际上,每个间隔开始后找到的第一个发射就是被推过的发射。source1 的 100 是在第一个间隔中找到的第一个发射。在下一个间隔中,source2 发出了 300,然后是 2000,接着是 4000。4000 正好在应用退出之前发出,因此我们得到了 throttleFirst() 的四个发射,而不是 throttleLast() 的三个。
除了在每个间隔发出第一个项目而不是最后一个项目之外,throttleLast() 的所有行为也适用于 throttleFirst()。指定较短的间隔会产生更多的发射,而较长的间隔会产生较少的发射。
throttleFirst() 和 throttleLast() 都会在计算 Scheduler 上发出,但你也可以将你自己的 Scheduler 作为第三个参数指定。
throttleWithTimeout() / debounce()
如果你玩 throttleFirst() 和 throttleLast(),你可能会对它们行为的一个方面感到不满意。它们对发射频率的变化是盲目的,并且它们只是简单地“插入”在固定的时间间隔,并拉取它们找到的第一个或最后一个发射。没有等待“沉默期”的概念,其中发射暂时停止,这可能是一个推进最后发生发射的好时机。
想象一下好莱坞动作电影,其中主角在遭受猛烈的枪战中。当子弹横飞时,他/她必须躲避并无法行动。但是,当他们的攻击者停下来装弹时,会有一个沉默期,他们有时间做出反应。这正是 throttleWithTimeout() 所做的。当发射快速发生时,它不会发出任何东西,直到有一个“沉默期”,然后它会推进最后的发射。
throttleWithTimout()(也称为 debounce())接受时间间隔参数,指定在最后一次发射可以推进之前必须有一段多长时间的不活跃期(这意味着没有发射来自源)。在我们的早期示例中,我们的三个连接的 Observable.interval() 源以每 100 毫秒的速度快速发射,然后是大约 2 秒的 300 毫秒爆发。但之后,间隔减慢到每 2 秒一次。如果我们只想在 1 秒的静默期后发射,我们不会发射任何东西,直到我们达到第三个 Observable.interval(),它每 2 秒发射一次,如下所示:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<String> source1 = Observable.interval(100, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 100) // map to elapsed time
.map(i -> "SOURCE 1: " + i)
.take(10);
Observable<String> source2 = Observable.interval(300, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 300) // map to elapsed time
.map(i -> "SOURCE 2: " + i)
.take(3);
Observable<String> source3 = Observable.interval(2000, TimeUnit.MILLISECONDS)
.map(i -> (i + 1) * 2000) // map to elapsed time
.map(i -> "SOURCE 3: " + i)
.take(2);
Observable.concat(source1, source2, source3)
.throttleWithTimeout(1, TimeUnit.SECONDS)
.subscribe(System.out::println);
sleep(6000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
SOURCE 2: 900
SOURCE 3: 2000
SOURCE 3: 4000
当 source3 开始时,source2 的 900 次发射就是最后一次发射,因为 source3 将不会在 2 秒内推送其第一次发射,这为 900 次发射提供了超过所需的 1 秒静默期。然后,2000 次发射紧接着发生,1 秒后没有进一步的发射发生,因此它被 throttleWithTimeout() 推进。另一秒后,4000 次发射被推送,2 秒的静默期(在程序退出之前)也允许它发射。
throttleWithTimeout() 方法是一种处理过多输入(例如用户快速点击按钮)和其他噪声、冗余事件的有效方式,这些事件会偶尔加速、减速或停止。throttleWithTimeout() 的唯一缺点是它将延迟每个获胜的发射。如果一个发射成功通过了 throttleWithTimeout(),它将被延迟指定的时间间隔,以确保没有更多的发射。特别是对于用户体验来说,这种人为的延迟可能不受欢迎。对于这些对延迟敏感的情况,一个更好的选择可能是利用 switchMap(),我们将在下一节中介绍。
切换
在 RxJava 中,有一个功能强大的操作符称为 switchMap()。它的使用感觉像 flatMap(),但它有一个重要的行为差异:它将从最新的发射中派生的最新 Observable 发射,并丢弃任何之前正在处理的 Observable。换句话说,它允许你取消一个正在发射的 Observable 并切换到新的一个,防止过时或冗余的处理。
假设我们有一个发射九个字符串的过程,并且它将每个字符串发射的延迟随机设置为 0 到 2000 毫秒。这是为了模拟对每个字符串进行的密集计算,如下所示:
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<String> items = Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon",
"Zeta", "Eta", "Theta", "Iota");
//delay each String to emulate an intense calculation
Observable<String> processStrings = items.concatMap(s ->
Observable.just(s)
.delay(randomSleepTime(), TimeUnit.MILLISECONDS)
);
processStrings.subscribe(System.out::println);
//keep application alive for 20 seconds
sleep(20000);
}
public static int randomSleepTime() {
//returns random sleep time between 0 to 2000 milliseconds
return ThreadLocalRandom.current().nextInt(2000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
Alpha
Beta
Gamma
Delta
Epsilon
Zeta
Eta
Theta
Iota
如你所见,每次发射需要 0-2 秒的时间来发射,处理所有字符串可能需要长达 20 秒。
假设我们想要每 5 秒运行这个过程,但我们想取消(或更技术性地,dispose())之前的过程实例,并且只运行最新的一个。使用switchMap()可以轻松做到这一点。在这里,我们创建另一个Observable.interval(),每 5 秒发射一次,然后我们使用switchMap()将其映射到我们想要处理Observable(在这种情况下是processStrings)。每 5 秒,进入switchMap()的发射将立即销毁当前正在处理的Observable(如果有),然后从它映射的新Observable中发射。为了证明dispose()被调用,我们将在switchMap()内部的Observable上放置doOnDispose()来显示一条消息:
import io.reactivex.Observable;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable<String> items = Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon",
"Zeta", "Eta", "Theta", "Iota");
//delay each String to emulate an intense calculation
Observable<String> processStrings = items.concatMap(s ->
Observable.just(s)
.delay(randomSleepTime(), TimeUnit.MILLISECONDS)
);
//run processStrings every 5 seconds, and kill each previous instance to start next
Observable.interval(5, TimeUnit.SECONDS)
.switchMap(i ->
processStrings
.doOnDispose(() -> System.out.println("Disposing! Starting next"))
).subscribe(System.out::println);
//keep application alive for 20 seconds
sleep(20000);
}
public static int randomSleepTime() {
//returns random sleep time between 0 to 2000 milliseconds
return ThreadLocalRandom.current().nextInt(2000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下(你的输出将不同):
Alpha
Beta
Gamma
Delta
Epsilon
Zeta
Eta
Disposing! Starting next
Alpha
Beta
Gamma
Delta
Disposing! Starting next
Alpha
Beta
Gamma
Delta
Disposing! Starting next
再次强调,switchMap()就像flatMap()一样,但它将取消任何之前正在处理的 Observables,并且只追踪最新的一个。这在许多情况下都有助于防止冗余或过时的工作,并且在用户界面中,快速的用户输入产生过时请求时尤其有效。你可以用它来取消数据库查询、网络请求和其他昂贵的任务,并用新任务替换它们。
为了使switchMap()有效,将发射推入switchMap()的线程不能被switchMap()内部的工作占用。这意味着你可能需要在switchMap()内部使用observeOn()或subscribeOn()在不同的线程上执行工作。如果switchMap()内部的操作难以停止(例如,使用 RxJava-JDBC 的数据库查询),你可能还想使用unsubscribeOn(),以防止触发线程在销毁时被占用。
你可以在switchMap()(不立即提供新工作)内取消工作的一个小技巧是条件性地返回Observable.empty()。这有助于取消长时间运行或无限过程。例如,如果你将 RxJavaFX (github.com/ReactiveX/RxJavaFX)作为依赖项引入,我们可以快速创建一个使用switchMap()的计时器应用程序,如下面的代码片段所示:
import io.reactivex.Observable;
import io.reactivex.rxjavafx.observables.JavaFxObservable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.concurrent.TimeUnit;
public final class JavaFxApp extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
Label counterLabel = new Label("");
ToggleButton startStopButton = new ToggleButton();
// Multicast the ToggleButton's true/false selected state
Observable<Boolean> selectedStates =
JavaFxObservable.valuesOf(startStopButton.selectedProperty())
.publish()
.autoConnect(2);
// Using switchMap() with ToggleButton's selected state will drive
// whether to kick off an Observable.interval(),
// or dispose() it by switching to empty Observable
selectedStates.switchMap(selected -> {
if (selected)
return Observable.interval(1, TimeUnit.MILLISECONDS);
else
return Observable.empty();
}).observeOn(JavaFxScheduler.platform()) // Observe on JavaFX UI thread
.map(Object::toString)
.subscribe(counterLabel::setText);
// Change ToggleButton's text depending on its state
selectedStates.subscribe(selected ->
startStopButton.setText(selected ? "STOP" : "START")
);
root.getChildren().addAll(counterLabel, startStopButton);
stage.setScene(new Scene(root));
stage.show();
}
}
前面的代码生成了一个使用switchMap()的计时器应用程序,如下面图 7.1 所示:

图 7.1 - 使用 switchMap()的计时器应用程序
按压ToggleButton将开始和停止计时器,它以毫秒为单位显示。注意,ToggleButton将通过一个名为selectedStates的Observable发出布尔值True/False。我们通过share()进行多播以防止 JavaFX 上的重复监听器,并且我们有两个观察者。第一个将使用switchMap()对每个布尔值进行操作,其中true将每毫秒从Observable.interval()发出,而false将通过替换为Observable.empty()来取消它。由于Observable.interval()将在Scheduler计算上发出,我们将使用observeOn()将其放回由 RxJavaFX 提供的 JavaFX Scheduler上。另一个观察者将根据其状态将ToggleButton的文本更改为 STOP 或 START。
组合按键
我们将通过整合我们所学的大部分内容来结束本章,完成一个复杂任务:将快速连续发生的按键组合成字符串,而没有任何延迟!这在用户界面中可以立即根据输入的内容“跳转”到列表中的项目,或者以某种方式执行自动完成。这可能是一个具有挑战性的任务,但正如我们将看到的,使用 RxJava 并不那么困难。
这个练习将再次使用 JavaFX 和 RxJavaFX。我们的用户界面将简单地有一个Label,它接收我们输入的按键的滚动连接。但 300 毫秒后,它将重置并接收一个空的""来清除它。以下是实现此功能的代码,以及一些当我在输入"Hello"后稍后输入"World"时的控制台输出截图:
import io.reactivex.Observable;
import io.reactivex.rxjavafx.observables.JavaFxObservable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.concurrent.TimeUnit;
public final class JavaFxApp extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
root.setMinSize(200, 100);
Label typedTextLabel = new Label("");
root.getChildren().addAll(typedTextLabel);
Scene scene = new Scene(root);
// Multicast typed keys
Observable<String> typedLetters =
JavaFxObservable.eventsOf(scene, KeyEvent.KEY_TYPED)
.map(KeyEvent::getCharacter)
.share();
// Signal 300 milliseconds of inactivity
Observable<String> restSignal =
typedLetters
.throttleWithTimeout(500, TimeUnit.MILLISECONDS)
.startWith(""); //trigger initial
// switchMap() each period of inactivity to
// an infinite scan() concatenating typed letters
restSignal.switchMap(s ->
typedLetters.scan("", (rolling, next) -> rolling + next)
).observeOn(JavaFxScheduler.platform())
.subscribe(s -> {
typedTextLabel.setText(s);
System.out.println(s);
});
stage.setScene(scene);
stage.show();
}
}
输出如下:
H
He
Hel
Hell
Hello
W
Wo
Wor
Worl
World
这是渲染的 UI:

当你输入按键时,Label将在 UI 和控制台上实时显示按键字符的滚动连接字符串。注意,在 500 毫秒的无活动后,它将重置并发出一个新的scan()操作,并丢弃旧的,从空字符串""开始。这可以在用户输入时立即发送搜索请求或自动完成建议,非常有帮助。
它的工作方式是我们有一个Observable发出键盘上按下的字符,但它通过share()进行多播,用于两个目的。首先,它用于创建另一个Observable,在 500 毫秒的无活动后发出最后输入的字符。但我们更关心的是发射的时机,它表示 500 毫秒的无活动已经发生。然后我们使用switchMap()将其映射到再次发出字符的Observable上,并无限地连续连接每个输入的字符,并发出每个结果字符串。然而,这个switchMap()中的scan()操作将在 500 毫秒的无活动发生时被丢弃,并使用一个新的scan()实例重新开始。
如果你觉得这个例子令人头晕,请慢慢来,继续学习。最终你会恍然大悟,一旦你做到了,你就真正掌握了本章中的思想!
摘要
在本章中,你学习了如何利用缓冲、窗口、节流和切换来应对快速发射的 Observables。理想情况下,当我们看到 Observables 的发射速度超过了 Observers 的处理能力时,我们应该利用 Flowables 和背压。我们将在下一章中学习这一点。但对于无法使用背压的情况,例如用户输入或定时器事件,你可以利用这三种操作类别来限制向下传递的发射数量。
在下一章中,我们将学习如何使用 Flowables 进行背压,这提供了更主动的方式来应对快速发射压倒 Observers 的常见情况。
第八章:Flowables 和 背压
在上一章中,我们学习了不同的操作符,它们可以拦截快速发射的发射,并合并或省略它们以减少传递到下游的发射。但对于大多数情况下,如果源头产生的发射比下游处理得快,最好首先让源头减速,并以与下游操作一致的速度发射。这被称为背压或流控制,可以通过使用 Flowable 而不是 Observable 来启用。这将是本章我们将与之工作的核心类型,我们将学习在应用程序中何时利用它。本章我们将涵盖以下主题:
-
理解背压
-
Flowable和Subscriber -
使用
Flowable.create() -
Observables 和 Flowables 的互操作性
-
背压操作符
-
使用
Flowable.generate()
理解背压
在整本书中,我强调了 Observables 的“基于推送”的特性。从源头同步且逐个推送项目到 Observer 确实是 Observable 链默认的工作方式,没有任何并发。
例如,以下是一个将发出从 1 到 999,999,999 的数字的 Observable。它将每个整数映射到一个 MyItem 实例,该实例简单地将其作为属性持有。但让我们在 Observer 中将每个发射的处理速度减慢 50 毫秒。这表明即使下游正在缓慢处理每个发射,上游也会同步地跟上它。这是因为只有一个线程在做所有的工作:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.range(1, 999_999_999)
.map(MyItem::new)
.subscribe(myItem -> {
sleep(50);
System.out.println("Received MyItem " + myItem.id);
});
}
static void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static final class MyItem {
final int id;
MyItem(int id) {
this.id = id;
System.out.println("Constructing MyItem " + id);
}
}
}
输出如下:
Constructing MyItem 1
Received MyItem 1
Constructing MyItem 2
Received MyItem 2
Constructing MyItem 3
Received MyItem 3
Constructing MyItem 4
Received MyItem 4
Constructing MyItem 5
Received MyItem 5
Constructing MyItem 6
Received MyItem 6
Constructing MyItem 7
Received MyItem 7
...
输出的 Constructing MyItem 和 Received MyItem 之间的交替表明每个发射都是从源头逐个处理到终端 Observer 的。这是因为只有一个线程为整个操作做所有的工作,使得一切同步。消费者和生产者以序列化、一致的方式传递发射。
需要背压的示例
当你向 Observable 链(尤其是 observeOn()、并行化以及如 delay() 这样的操作符)添加并发操作时,操作变为 异步。这意味着在给定时间内,Observable 链的多个部分可以处理发射,生产者可以超过消费者,因为它们现在在不同的线程上操作。发射不再严格地从源头逐个传递到 Observer,然后再开始下一个。这是因为一旦发射通过 observeOn()(或其他并发操作符)击中不同的 Scheduler,源头就不再负责将那个发射推送到 Observer。因此,源头将开始推送下一个发射,即使前一个发射可能还没有到达 Observer。
如果我们将之前的例子添加 observeOn(Shedulers.io()) 在 subscribe() 之前(如下面的代码所示),你会注意到一个非常明显的事实:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
Observable.range(1, 999_999_999)
.map(MyItem::new)
.observeOn(Schedulers.io())
.subscribe(myItem -> {
sleep(50);
System.out.println("Received MyItem " + myItem.id);
});
sleep(Long.MAX_VALUE);
}
static void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static final class MyItem {
final int id;
MyItem(int id) {
this.id = id;
System.out.println("Constructing MyItem " + id);
}
}
}
输出如下:
...
Constructing MyItem 1001899
Constructing MyItem 1001900
Constructing MyItem 1001901
Constructing MyItem 1001902
Received MyItem 38
Constructing MyItem 1001903
Constructing MyItem 1001904
Constructing MyItem 1001905
Constructing MyItem 1001906
Constructing MyItem 1001907
..
这只是我控制台输出的一个部分。注意,当创建 MyItem 1001902 时,Observer 仍在处理 MyItem 38。排放被推送的速度比 Observer 处理它们的速度快得多,并且由于排放积压在 observeOn() 中无限制地排队,这可能导致许多问题,包括 OutOfMemoryError 异常。
引入 Flowable
那我们应该如何减轻这种情况呢?你可以尝试使用原生的 Java 并发工具,例如信号量。但幸运的是,RxJava 为此问题提供了一个简化的解决方案:Flowable。Flowable 是 Observable 的背压变体,它告诉源以下游操作指定的速度发出。
在下面的代码中,将 Observable.range() 替换为 Flowable.range(),这将使整个链使用 Flowable 而不是 Observable 来工作。运行代码,你将看到输出有非常大的不同:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.Flowable;
public class Launcher {
public static void main(String[] args) {
Flowable.range(1, 999_999_999)
.map(MyItem::new)
.observeOn(Schedulers.io())
.subscribe(myItem -> {
sleep(50);
System.out.println("Received MyItem " + myItem.id);
});
sleep(Long.MAX_VALUE);
}
static void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static final class MyItem {
final int id;
MyItem(int id) {
this.id = id;
System.out.println("Constructing MyItem " + id);
}
}
}
输出如下:
Constructing MyItem 1
Constructing MyItem 2
Constructing MyItem 3
...
Constructing MyItem 127
Constructing MyItem 128
Received MyItem 1
Received MyItem 2
Received MyItem 3
...
Received MyItem 95
Received MyItem 96
Constructing MyItem 129
Constructing MyItem 130
Constructing MyItem 131
...
Constructing MyItem 223
Constructing MyItem 224
Received MyItem 97
Received MyItem 98
Received MyItem 99
...
注意,Flowable 不使用观察者(Observers)订阅,而是使用订阅者(Subscribers),我们将在稍后深入探讨。
当使用 Flowable 时,你会注意到输出有非常大的不同。我使用 ... 省略了前面输出的一部分,以突出一些关键事件。Flowable.range() 立即推送了 128 个排放,构建了 128 个 MyItem 实例。之后,observeOn() 将其中的 96 个推送到了 Subscriber。在这 96 个排放被 Subscriber 处理之后,又有 96 个从源推送出来。然后又有 96 个传递给了 Subscriber。
你看到模式了吗?源开始时推送了 128 个排放,之后,Flowable 链以每次 96 个排放的稳定流进行处理。这几乎就像整个 Flowable 链努力在任何给定时间不超过 96 个排放在其管道中。实际上,这正是正在发生的事情!这就是我们所说的 背压,它有效地引入了拉动态到基于推送的操作中,以限制源发出的频率。
但为什么Flowable.range()从 128 个发射开始,为什么observeOn()在请求另一个 96 个之前只向下发送 96 个,留下了 32 个未处理发射?初始的发射批次稍微大一些,所以如果有任何空闲时间,会有一些额外的工作被排队。如果在理论上我们的Flowable操作从请求 96 个发射开始,并继续每次发射 96 个发射,那么可能会有一些时刻操作可能会空闲等待下一个 96 个。因此,维护一个额外的 32 个发射的滚动缓存,在空闲时刻提供工作,这可以提供更高的吞吐量。这就像一个仓库在等待从工厂得到更多库存的同时,持有少量额外的库存来供应订单。
Flowable及其操作符的伟大之处在于它们通常为你做所有工作。除非你需要从头创建自己的Flowable或处理(如Observable)不实现背压的源,否则你不需要指定任何背压策略或参数。我们将在本章的其余部分讨论这些情况,并希望你不会经常遇到。
否则,Flowable就像我们迄今为止学到的几乎所有操作符的Observable。你可以从Observable转换为Flowable,反之亦然,我们将在后面讨论。但首先,让我们讨论我们应该在什么情况下使用Flowable而不是Observable。
何时使用 Flowables 和背压
知道何时使用Flowable而不是Observable是至关重要的。总的来说,Flowable提供的优势是更节省内存的使用(防止OutOfMemoryError异常)以及防止MissingBackpressureException。后者可能发生在操作对源进行背压,但源在其实现中没有背压协议的情况下。然而,Flowable的缺点是它增加了开销,可能不如Observable快速。
这里有一些指南可以帮助你选择使用Observable还是Flowable。
如果使用一个可观察对象...
-
你预计在
Observable订阅的生命周期中发射的数量很少(少于 1000)或者发射是间歇性的且间隔很远。如果你只期望从源处发出少量的发射,一个Observable就能很好地完成任务并且开销更小。但是当你处理大量数据并对它们执行复杂操作时,你可能会想使用Flowable。 -
你的操作是严格同步的,并且并发使用有限。这包括在
Observable链的开始简单使用subscribeOn(),因为该过程仍在单个线程上操作并同步向下发射项目。然而,当你开始在不同的线程上压缩和组合不同的流、并行化或使用如observeOn()、interval()和delay()这样的操作符时,你的应用程序就不再是同步的,你可能更倾向于使用Flowable。 -
你想要在 Android、JavaFX 或 Swing 上发射用户界面事件,如按钮点击、
ListView选择或其他用户输入。由于用户不能被编程告知减慢速度,所以很少有机会使用Flowable。为了应对快速的用户输入,你可能会更倾向于使用第七章中讨论的操作符,切换、节流、窗口化和缓冲。
在以下情况下使用 Flowable...
-
你正在处理超过 10,000 个元素,并且源有机会以有规律的方式生成排放。当源是异步的并且推送大量数据时,这一点尤其正确。
-
你想要从支持阻塞并返回结果的 IO 操作中发射,这是许多 IO 源的工作方式。迭代记录的数据源,如文件中的行或 JDBC 中的
ResultSet,特别容易控制,因为迭代可以根据需要暂停和恢复。可以请求一定量返回结果的网络和流式 API 也可以轻松地产生背压。
注意在 RxJava 1.0 中,Observable会受到背压,这本质上就是 RxJava 2.0 中的Flowable。Flowable和Observable成为不同类型的原因是它们在不同情况下的优点,如前所述。
你会发现你可以轻松地将 Observables 和 Flowables 一起使用。但你需要小心并意识到它们被使用的上下文以及可能出现的未期望的瓶颈。
理解 Flowable 和 Subscriber
几乎所有你到目前为止学到的Observable工厂和操作符也适用于 Flowable。在工厂方面,有Flowable.range()、Flowable.just()、Flowable.fromIterable()和Flowable.interval()。其中大多数为你实现了背压,并且使用方式通常与Observable等价。
然而,考虑一下Flowable.interval(),它在固定的时间间隔推动基于时间的排放。这能否在逻辑上产生背压?思考这样一个事实:每次排放都与它排放的时间紧密相关。如果我们减慢Flowable.interval()的速度,我们的排放将不再反映时间间隔,从而产生误导。因此,Flowable.interval()是标准 API 中少数几个在下游请求背压时可以抛出MissingBackpressureException的情况之一。在这里,如果我们每毫秒对observeOn()之后的慢速intenseCalculation()进行排放,我们将得到这个错误:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Flowable.interval(1, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.io())
.map(i -> intenseCalculation(i))
.subscribe(System.out::println, Throwable::printStackTrace);
sleep(Long.MAX_VALUE);
}
public static <T> T intenseCalculation(T value) {
sleep(ThreadLocalRandom.current().nextInt(3000));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
0
io.reactivex.exceptions.MissingBackpressureException: Cant deliver value 128 due to lack of requests
at io.reactivex.internal.operators.flowable.FlowableInterval
...
为了克服这个问题,你可以使用onBackpresureDrop()或onBackPressureBuffer()等操作符,我们将在本章后面学习。Flowable.interval()是那些在源头上逻辑上不能进行背压的工厂之一,因此你可以使用它后面的操作符来为你处理背压。否则,你使用的其他大多数Flowable工厂都支持背压。稍后,我们需要指出如何创建符合背压的自己的Flowable源,我们将在稍后讨论这个问题。但首先,我们将更深入地探讨Subscriber。
关于Subscriber
与Observer不同,Flowable使用Subscriber在Flowable链的末尾消费排放量和事件。如果你只传递 lambda 事件参数(而不是整个Subscriber对象),subscribe()不会返回Disposable,而是返回Subscription,可以通过调用cancel()而不是dispose()来取消它。Subscription还可以通过其request()方法向上游传达想要多少项。Subscription还可以在Subscriber的onSubscribe()方法中利用,以便在准备好接收排放量时立即request()元素。
就像Observer一样,创建Subscriber最快的方法是将 lambda 参数传递给subscribe(),正如我们之前所做的那样(以下代码再次展示了这一点)。这个Subscriber的默认实现将请求无界的排放量,但任何在其前面的操作员仍然会自动处理背压:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Flowable.range(1,1000)
.doOnNext(s -> System.out.println("Source pushed " + s))
.observeOn(Schedulers.io())
.map(i -> intenseCalculation(i))
.subscribe(s -> System.out.println("Subscriber received " + s),
Throwable::printStackTrace,
() -> System.out.println("Done!")
);
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
*//sleep up to 200 milliseconds*
sleep(ThreadLocalRandom.current().nextInt(200));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当然,你也可以实现自己的Subscriber,它当然有onNext()、onError()、onComplete()方法以及onSubscribe()。这不像实现Observer那样直接,因为你需要在适当的时候在Subscription上调用request()来请求排放量。
实现一个Subscriber最快和最简单的方法是在onSubscribe()方法中调用Subscription上的request(Long.MAX_VALUE),这本质上告诉上游“现在给我所有东西”。尽管前面的操作员将以自己的背压速度请求排放量,但最后操作员和Subscriber之间不会存在背压。这通常是正常的,因为上游操作员无论如何都会约束流量。
在这里,我们重新实现了我们之前的例子,但实现了我们自己的Subscriber:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
Flowable.range(1,1000)
.doOnNext(s -> System.out.println("Source pushed " + s))
.observeOn(Schedulers.io())
.map(i -> intenseCalculation(i))
.subscribe(new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Integer s) {
sleep(50);
System.out.println("Subscriber received " + s);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done!");
}
});
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
*//sleep up to 200 milliseconds*
sleep(ThreadLocalRandom.current().nextInt(200));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果你希望你的Subscriber与它前面的操作员建立显式的背压关系,你需要对request()调用进行微观管理。比如说,在某种极端情况下,你决定让Subscriber最初请求 40 个排放量,然后每次请求 20 个排放量。这是你需要做的:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
public class Launcher {
public static void main(String[] args) {
Flowable.range(1,1000)
.doOnNext(s -> System.out.println("Source pushed " + s))
.observeOn(Schedulers.io())
.map(i -> intenseCalculation(i))
.subscribe(new Subscriber<Integer>() {
Subscription subscription;
AtomicInteger count = new AtomicInteger(0);
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
System.out.println("Requesting 40 items!");
subscription.request(40);
}
@Override
public void onNext(Integer s) {
sleep(50);
System.out.println("Subscriber received " + s);
if (count.incrementAndGet() % 20 == 0 && count.get() >= 40)
System.out.println("Requesting 20 more!");
subscription.request(20);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done!");
}
});
sleep(20000);
}
public static <T> T intenseCalculation(T value) {
//sleep up to 200 milliseconds
sleep(ThreadLocalRandom.current().nextInt(200));
return value;
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Requesting 40 items!
Source pushed 1
Source pushed 2
...
Source pushed 127
Source pushed 128
Subscriber received 1
Subscriber received 2
...
Subscriber received 39
Subscriber received 40
Requesting 20 more!
Subscriber received 41
Subscriber received 42
...
Subscriber received 59
Subscriber received 60
Requesting 20 more!
Subscriber received 61
Subscriber received 62
...
Subscriber received 79
Subscriber received 80
Requesting 20 more!
Subscriber received 81
Subscriber received 82
...
注意,源仍然最初排放 128 个排放量,然后仍然每次推送 96 个排放量。但是我们的Subscriber只接收了 40 个排放量,正如指定的那样,然后持续要求再增加 20 个。我们Subscriber中的request()调用只与它上游的即时操作员map()通信。map()操作员可能将那个请求转发到observeOn(),它正在缓存项目,并且只根据Subscriber的要求输出 40 个和 20 个。当其缓存变低或清空时,它将从上游请求另一个 96 个。
这是一个警告:你不应该依赖于请求排放的确切数字,例如 128 和 96。这些是我们偶然观察到的内部实现,这些数字可能会在未来为了进一步优化实现而改变。
这种自定义实现实际上可能正在降低我们的吞吐量,但它展示了如何通过自己的Subscriber实现来管理自定义背压。只需记住,request()调用并不一直向上游传递。它们只到达前面的操作员,该操作员决定如何将那个请求转发到上游。
创建一个 Flowable
在这本书的早期,我们多次使用Observable.create()从头开始创建我们自己的Observable,这描述了在订阅时如何输出项目,如下面的代码片段所示:
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> source = Observable.create(emitter -> {
for (int i=0; i<=1000; i++) {
if (emitter.isDisposed())
return;
emitter.onNext(i);
}
emitter.onComplete();
});
source.observeOn(Schedulers.io())
.subscribe(System.out::println);
sleep(1000);
}
}
输出如下:
0
1
2
3
4
...
这个Observable.create()将输出从 0 到 1000 的整数,然后调用onComplete()。如果对从subscribe()返回的Disposable调用dispose(),它可以被突然停止,for 循环将检查这一点。
然而,思考一下,如果我们执行Flowable.create(),即Observable.create()的Flowable等效,如何实现类似这样的背压。使用像前面的简单 for 循环,没有基于下游Subscriber请求的排放停止和恢复的概念。正确实现背压将增加一些复杂性。有一些更简单的方法来支持背压,但它们通常涉及妥协策略,如缓冲和丢弃,我们将在首先介绍。还有一些工具可以在源处实现背压,我们将在之后介绍。
使用 Flowable.create()和 BackpressureStrategy
利用Flowable.create()创建一个Flowable感觉就像Observable.create(),但有一个关键的区别;你必须指定一个BackpressureStrategy作为第二个参数。这种可枚举类型并不提供任何形式的背压支持的魔法实现。事实上,这仅仅通过缓存或丢弃排放量或根本不实现背压来支持背压。
在这里,我们使用Flowable.create()来创建一个Flowable,但我们提供了一个第二个BackpressureStrategy.BUFFER参数来在背压之前缓存排放量:
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
Flowable<Integer> source = Flowable.create(emitter -> {
for (int i=0; i<=1000; i++) {
if (emitter.isCancelled())
return;
emitter.onNext(i);
}
emitter.onComplete();
}, BackpressureStrategy.BUFFER);
source.observeOn(Schedulers.io())
.subscribe(System.out::println);
sleep(1000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
0
1
2
3
4
...
这不是最佳方案,因为排放将被保留在一个无界队列中,当Flowable.create()推送过多的排放时,你可能会得到一个OutOfMemoryError。但至少它防止了MissingBackpressureException,并且可以在一定程度上使你的自定义Flowable可行。我们将在本章后面学习使用Flowable.generate()实现更健壮的反压方法。
目前有五种BackpressureStrategy选项可供选择。
| 反压策略 | 描述 |
|---|---|
| 缺失 | 实际上没有实现任何反压。下游必须处理反压溢出,当与我们在本章后面将要介绍的onBackpressureXXX()操作符一起使用时可能很有帮助。 |
| 错误 | 当下游无法跟上源时,会立即触发MissingBackpressureException。 |
| BUFFER | 在无界队列中排队排放,直到下游能够消费它们,但如果队列太大,可能会引起OutOfMemoryError。 |
| DROP | 如果下游无法跟上,这将忽略上游排放,并在下游忙碌时不会排队任何内容。 |
| 最新 | 这将只保留最新的排放,直到下游准备好接收它。 |
接下来,我们将看到一些这些策略作为操作符的使用,特别是将 Observables 转换为 Flowables。
将 Observable 转换为 Flowable(反之亦然)
你还可以通过将Observable转换为Flowable来对没有反压概念的数据源实现BackpressureStrategy。你可以通过调用其toFlowable()操作符并传递一个BackpressureStrategy作为参数来轻松地将 Observable 转换为Flowable。在下面的代码中,我们使用BackpressureStrategy.BUFFER将Observable.range()转换为Flowable。Observable 没有反压概念,所以它将尽可能快地推送项目,不管下游是否能跟上。但是,带有缓冲策略的toFlowable()将在下游无法跟上时作为代理来缓冲排放:
import io.reactivex.BackpressureStrategy;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
Observable<Integer> source = Observable.range(1,1000);
source.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.io())
.subscribe(System.out::println);
sleep(10000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
再次注意,带有缓冲策略的toFlowable()将有一个无界队列,这可能会导致OutOfMemoryError。在现实世界中,最好首先使用Flowable.range(),但有时你可能只能得到一个Observable。
Flowable还有一个toObservable()操作符,它将Flowable<T>转换为Observable<T>。这有助于使Flowable在Observable链中使用,特别是与flatMap()等操作符一起使用,如下面的代码所示:
import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
public class Launcher {
public static void main(String[] args) {
Flowable<Integer> integers =
Flowable.range(1, 1000)
.subscribeOn(Schedulers.computation());
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon")
.flatMap(s -> integers.map(i -> i + "-" + s).toObservable())
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果Observable<String>有超过五个排放(例如 1,000 或 10,000),那么将其转换为Flowable可能更好,而不是将扁平映射的Flowable转换为Observable。
即使你调用 toObservable(),Flowable 仍然会在上游利用背压。但到了它成为 Observable 的点,下游将不再受到背压,并请求 Long.MAX_VALUE 数量的发射。只要下游没有发生更复杂的操作或并发变化,并且上游的 Flowable 操作限制了发射的数量,这可能就足够了。
但通常情况下,当你承诺使用一个 Flowable 时,你应该努力使你的操作保持 Flowable。
使用 onBackpressureXXX() 操作符
如果你提供了一个没有背压实现(包括从 Observable 派生的)的 Flowable,你可以使用 onBackpressureXXX() 操作符应用 BackpressureStrategy。这些操作符还提供了一些额外的配置选项。如果,例如,你有一个 Flowable.interval() 发射速度比消费者处理速度快,这可能会很有用。Flowable.interval() 由于是时间驱动的,不能在源处减慢速度,但我们可以使用 onBackpressureXXX() 操作符在它和下游之间代理。我们将使用 Flowable.interval() 进行这些示例,但这可以应用于任何没有实现背压的 Flowable。
有时,Flowable 可能只是配置了 BackpressureStrategy.MISSING,这样 onBackpressureXXX() 操作符就可以稍后指定策略。
onBackPressureBuffer()
onBackPressureBuffer() 将接受一个假设未实现背压的现有 Flowable,并在该点向下游应用 BackpressureStrategy.BUFFER。由于 Flowable.interval() 在源处不能进行背压,将其放在 onBackPressureBuffer() 之后将代理一个背压队列到下游:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Flowable.interval(1, TimeUnit.MILLISECONDS)
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.subscribe(i -> {
sleep(5);
System.out.println(i);
});
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
0
1
2
3
4
5
6
7
...
你还可以提供一些重载参数。我们不会详细介绍所有这些参数,你可以参考 JavaDocs 获取更多信息,但我们将突出显示常见的参数。容量参数将为缓冲区创建一个最大阈值,而不是允许其无界。你可以指定一个 onOverflow Action lambda,当溢出超过容量时触发一个动作。你还可以指定一个 BackpressureOverflowStrategy 枚举来指示如何处理超过容量的溢出。
这里是你可以选择的三个 BackpressureOverflowStrategy 枚举项:
| BackpressureOverflowStrategy | 描述 |
|---|---|
| ERROR | 当容量超过时立即抛出错误 |
| DROP_OLDEST | 从缓冲区中丢弃最旧的值,为新值腾出空间 |
| DROP_LATEST | 从缓冲区中丢弃最新的值,以优先处理较旧的未消费值 |
在以下代码中,我们保持最大容量为 10,并在溢出时指定使用 BackpressureOverflowStrategy.DROP_LATEST。我们还会在溢出时打印一个通知:
import io.reactivex.BackpressureOverflowStrategy;
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Flowable.interval(1, TimeUnit.MILLISECONDS)
.onBackpressureBuffer(10,
() -> System.out.println("overflow!"),
BackpressureOverflowStrategy.DROP_LATEST)
.observeOn(Schedulers.io())
.subscribe(i -> {
sleep(5);
System.out.println(i);
});
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
...
overflow!
overflow!
135
overflow!
overflow!
overflow!
overflow!
overflow!
136
overflow!
overflow!
overflow!
overflow!
overflow!
492
overflow!
overflow!
overflow!
...
注意,在我嘈杂的输出这部分,136和492之间跳过了很大一段数字范围。这是因为由于BackpressureOverflowStrategy.DROP_LATEST,这些排放被从队列中丢弃。队列已经充满了等待被消费的排放,所以新的排放被忽略了。
onBackPressureLatest()
onBackpressureBuffer()的一个轻微变体是onBackPressureLatest()。当下游忙碌时,它将保留源的最新值,一旦下游空闲可以处理更多,它将提供最新值。在此忙碌期间发出的任何先前值都将丢失:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Flowable.interval(1, TimeUnit.MILLISECONDS)
.onBackpressureLatest()
.observeOn(Schedulers.io())
.subscribe(i -> {
sleep(5);
System.out.println(i);
});
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
...
122
123
124
125
126
127
494
495
496
497
...
如果你研究我的输出,你会注意到127和494之间有一个跳跃。这是因为所有介于它们之间的数字最终都被494这个最新值击败,当时,下游已经准备好处理更多的排放。它从消费缓存的494和其他之前被丢弃的排放开始。
onBackPressureDrop()
onBackpressureDrop()将简单地丢弃下游太忙无法处理的排放。当下游已经忙碌(例如,一个"RUN"请求被重复发送,尽管结果过程已经运行)时,这很有帮助。你可以选择性地提供一个onDrop lambda 参数,指定对每个丢弃项要做什么,我们将简单地按照以下代码打印出来:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Flowable.interval(1, TimeUnit.MILLISECONDS)
.onBackpressureDrop(i -> System.out.println("Dropping " + i))
.observeOn(Schedulers.io())
.subscribe(i -> {
sleep(5);
System.out.println(i);
});
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
...
Dropping 653
Dropping 654
Dropping 655
Dropping 656
127
Dropping 657
Dropping 658
Dropping 659
Dropping 660
Dropping 661
493
Dropping 662
Dropping 663
Dropping 664
...
在我的输出中,注意127和493之间有一个很大的跳跃。它们之间的数字被丢弃,因为当它们准备好被处理时,下游已经忙碌,所以它们被丢弃而不是排队。
使用Flowable.generate()
本章到目前为止我们讨论的很多内容都没有展示出对源进行背压的最优方法。是的,使用Flowable以及大多数标准工厂和操作符会自动为你处理背压。然而,如果你正在创建自己的自定义源,Flowable.create()或onBackPressureXXX()操作符在处理背压请求方面有些妥协。虽然对于某些情况来说既快又有效,但缓存排放或简单地丢弃它们并不总是可取的。最好是首先让源受到背压。
幸运的是,Flowable.generate()存在,可以帮助创建背压,在很好地抽象级别上尊重源。它将接受一个Consumer<Emitter<T>>,就像Flowable.create()一样,但它将使用 lambda 来指定每次从上游请求项目时要传递的onNext()、onComplete()和onError()事件。
在你使用 Flowable.generate() 之前,考虑将你的源 Iterable<T> 改为 Iterable<T> 并将其传递给 Flowable.fromIterable()。Flowable.fromIterable() 会尊重背压,并且对于许多情况来说可能更容易使用。否则,如果你需要更具体的东西,Flowable.generate() 是你的下一个最佳选择。
Flowable.generate() 的最简单重载只接受 Consumer<Emitter<T>> 并假设在发射之间没有维护状态。这有助于创建一个具有背压感知的随机整数生成器,如下所示。请注意,立即发出 128 个发射,但之后,在从源发送另一个 96 个之前,会向下游推送 96 个:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.ThreadLocalRandom;
public class Launcher {
public static void main(String[] args) {
randomGenerator(1,10000)
.subscribeOn(Schedulers.computation())
.doOnNext(i -> System.out.println("Emitting " + i))
.observeOn(Schedulers.io())
.subscribe(i -> {
sleep(50);
System.out.println("Received " + i);
});
sleep(10000);
}
static Flowable<Integer> randomGenerator(int min, int max) {
return Flowable.generate(emitter ->
emitter.onNext(ThreadLocalRandom.current().nextInt(min, max))
);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
...
Emitting 8014
Emitting 3112
Emitting 5958
Emitting 4834 //128th emission
Received 9563
Received 4359
Received 9362
...
Received 4880
Received 3192
Received 979 //96th emission
Emitting 8268
Emitting 3889
Emitting 2595
...
使用 Flowable.generate(),在 Consumer<Emitter<T>> 中调用多个 onNext() 操作符将导致 IllegalStateException。下游只需要调用一次 onNext(),因此它可以按照需要重复调用,以保持流动。如果发生异常,它还会为您发出 onError()。
您还可以提供一个类似于 reduce() 的“种子”状态,以维护从一次发射到下一次发射传递的状态。假设我们想要创建类似于 Flowable.range() 的东西,但相反,我们想要在 upperBound 和 lowerBound 之间反向发出整数。使用 AtomicInteger 作为我们的状态,我们可以递减它并将它的值传递给发射器的 onNext() 操作符,直到遇到 lowerBound。这如下所示:
import io.reactivex.Flowable;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.atomic.AtomicInteger;
public class Launcher {
public static void main(String[] args) {
rangeReverse(100,-100)
.subscribeOn(Schedulers.computation())
.doOnNext(i -> System.out.println("Emitting " + i))
.observeOn(Schedulers.io())
.subscribe(i -> {
sleep(50);
System.out.println("Received " + i);
});
sleep(50000);
}
static Flowable<Integer> rangeReverse(int upperBound, int lowerBound) {
return Flowable.generate(() -> new AtomicInteger(upperBound + 1),
(state, emitter) -> {
int current = state.decrementAndGet();
emitter.onNext(current);
if (current == lowerBound)
emitter.onComplete();
}
);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
Emitting 100
Emitting 99
...
Emitting -25
Emitting -26
Emitting -27 //128th emission
Received 100
Received 99
Received 98
...
Received 7
Received 6
Received 5 // 96th emission
Emitting -28
Emitting -29
Emitting -30
Flowable.generator() 提供了一个很好地抽象化的机制来创建一个尊重背压的源。因此,如果你不希望与缓存或丢弃发射项打交道,你可能会更喜欢使用这个而不是 Flowable.create()。
使用 Flowable.generate(),您还可以提供一个第三个 Consumer<? super S> disposeState 参数,在终止时执行任何清理操作,这对于 IO 源可能很有帮助。
摘要
在本章中,你学习了关于 Flowable 和背压以及它应该在哪些情况下优先于 Observable。当并发进入你的应用程序并且大量数据可以通过它流动时,Flowables 特别受欢迎,因为它调节了在给定时间内从源处来的数据量。一些 Flowables,如 Flowable.interval() 或从 Observable 派生的那些,没有实现背压。在这些情况下,你可以使用 onBackpressureXXX() 操作符来排队或丢弃下游的发射。如果你是从头开始创建自己的 Flowable 源,则更喜欢使用现有的 Flowable 工厂,如果那失败了,则更喜欢使用 Flowable.generate() 而不是 Flowable.create()。
如果你已经到达这个阶段并且理解了这本书到目前为止的大部分内容,恭喜你!你已经拥有了 RxJava 的核心概念,而本书的剩余部分对你来说将是一条轻松的道路。下一章将介绍如何创建自己的操作符,这可能会是一项相对复杂的工作。至少,你应该知道如何组合现有的操作符来创建新的操作符,这将是下一个话题之一。
第九章:Transformers 和自定义操作符
在 RxJava 中,有使用compose()和lift()方法实现自定义操作符的方法,这两个方法都存在于Observable和Flowable上。大多数时候,你可能会想要组合现有的 RxJava 操作符来创建一个新的操作符。但有时,你可能需要从头开始构建的操作符。后者工作量更大,但我们将介绍如何完成这两个任务。
在本章中,我们将介绍以下主题:
-
使用
compose()和 Transformers 组合现有操作符 -
to()操作符 -
使用
lift()从头实现操作符 -
RxJava2-Extras 和 RxJava2Extensions
Transformers
当使用 RxJava 时,你可能希望重用Observable或Flowable链的一部分,并以某种方式将这些操作符合并成一个新的操作符。优秀的开发者会寻找重用代码的机会,RxJava 通过ObservableTransformer和FlowableTransformer提供了这种能力,你可以将这些传递给compose()操作符。
ObservableTransformer
将Google Guava作为依赖项恢复。在第三章“基本操作符”中,我们介绍了collect()操作符,并使用它将Observable<T>转换为Single<ImmutableList<T>>。实际上,我们希望将T的发射项收集到 Google Guava 的ImmutableList<T>中。假设我们重复进行此操作足够多次,直到它开始显得冗余。在这里,我们使用这个ImmutableList操作来处理两个不同的Observable订阅:
import com.google.common.collect.ImmutableList;
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.collect(ImmutableList::builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.subscribe(System.out::println);
Observable.range(1,15)
.collect(ImmutableList::builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.subscribe(System.out::println);
}
}
输出如下:
[Alpha, Beta, Gamma, Delta, Epsilon]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
看看上面两个地方使用的 Observable 链的这部分:
collect(ImmutableList::builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
两次调用有点冗余,那么我们能否将这些操作符组合成一个单独的操作符,该操作符将发射项收集到ImmutableList中?实际上,可以!为了针对Observable<T>,你可以实现ObservableTransformer<T,R>。这个类型有一个apply()方法,它接受上游的Observable<T>并返回下游的Observable<R>。在你的实现中,你可以返回一个添加了任何操作符到上游的Observable链,并在这些转换之后返回一个Observable<R>。
对于我们的示例,我们将针对给定的Observable<T>的任何通用类型T,R将是通过Observable<ImmutableList<T>>发射的ImmutableList<T>。我们将把这个所有东西打包在一个ObservableTransformer<T,ImmutableList<T>>实现中,如下面的代码片段所示:
public static <T> ObservableTransformer<T, ImmutableList<T>> toImmutableList() {
return new ObservableTransformer<T, ImmutableList<T>>() {
@Override
public ObservableSource<ImmutableList<T>> apply(Observable<T> upstream) {
return upstream.collect(ImmutableList::<T>builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.toObservable(); *// must turn Single into Observable*
}
};
}
由于collect()返回一个Single,因此我们需要在它上面调用toObservable(),因为ObservableTransformer期望返回一个Observable,而不是Single。Transformers 通过静态工厂方法提供的情况并不少见,所以我们在这里就是这样做的。
由于ObservableTransformer中只有一个抽象方法,我们可以使用 lambda 表达式来简化这一点。这样读起来更容易,因为它从左到右/从上到下读取,并表达出“对于给定的上游 Observable,返回添加了这些操作符的下游 Observable”:
public static <T> ObservableTransformer<T, ImmutableList<T>> toImmutableList() {
return upstream -> upstream.collect(ImmutableList::<T>builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.toObservable(); *// must turn Single into Observable*
}
要将一个 Transformer 加入到Observable链中,你需要将它传递给compose()操作符。当在Observable<T>上调用时,compose()操作符接受一个ObservableTransformer<T,R>并返回转换后的Observable<R>。这允许你在多个地方重用 Rx 逻辑,现在我们可以在两个Observable操作上调用compose(toImmutableList()):
import com.google.common.collect.ImmutableList;
import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.compose(toImmutableList())
.subscribe(System.out::println);
Observable.range(1,10)
.compose(toImmutableList())
.subscribe(System.out::println);
}
public static <T> ObservableTransformer<T, ImmutableList<T>> toImmutableList() {
return upstream -> upstream.collect(ImmutableList::<T>builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.toObservable(); *// must turn Single into Observable*
}
}
输出如下:
[Alpha, Beta, Gamma, Delta, Epsilon]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
在 API 中,将 Transformers 组织在静态工厂类中是很常见的。在实际应用中,你可能会在GuavaTransformers类中存储你的toImmutableList() Transformer。然后,你可以在Observable操作中通过调用compose(GuavaTransformers.toImmutableList())来调用它。
注意:在这个例子中,实际上我们可以将toImmutableList()制作成一个可重用的单例,因为它不接受任何参数。
你也可以创建针对特定发射类型并接受参数的 Transformers。例如,你可以创建一个接受分隔符参数并将String发射与该分隔符连接的joinToString() Transformer。只有当在Observable<String>上调用时,此ObservableTransformer的使用才会编译:
import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.compose(joinToString("/"))
.subscribe(System.out::println);
}
public static ObservableTransformer<String, String> joinToString(String separator) {
return upstream -> upstream
.collect(StringBuilder::new, (b,s) -> {
if (b.length() == 0)
b.append(s);
else
b.append(separator).append(s);
})
.map(StringBuilder::toString)
.toObservable();
}
}
输出如下:
Alpha/Beta/Gamma/Delta/Epsilon
Transformers 是重用一系列执行共同任务的操作符的绝佳方式,利用它们可以极大地提高你的 Rx 代码的可重用性。通常,通过实现静态工厂方法,你可以获得最大的灵活性和速度,但你也可以将ObservableTransformer扩展到你的类实现中。
正如我们将在第十二章中学习的那样,使用 Kotlin 与 RxJava,Kotlin 语言提供了强大的语言特性,可以进一步简化 RxJava。你不需要使用 Transformers,而是可以利用扩展函数将操作符添加到Observable和Flowable类型,而无需继承。我们将在稍后了解更多关于这一点。
FlowableTransformer
当你实现自己的ObservableTransformer时,你可能还想创建一个FlowableTransformer对应物。这样,你就可以在你的操作符上使用 Observables 和 Flowables。
FlowableTransformer与ObservableTransformer没有太大区别。当然,它将支持背压,因为它与 Flowables 组合。在其他方面,它的使用几乎相同,只是你显然需要在Flowable上而不是Observable上传递它到compose()。
在这里,我们将我们的toImmutableList()方法返回一个ObservableTransformer,并将其实现为FlowableTransformer:
import com.google.common.collect.ImmutableList;
import io.reactivex.Flowable;
import io.reactivex.FlowableTransformer;
public class Launcher {
public static void main(String[] args) {
Flowable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.compose(toImmutableList())
.subscribe(System.out::println);
Flowable.range(1,10)
.compose(toImmutableList())
.subscribe(System.out::println);
}
public static <T> FlowableTransformer<T, ImmutableList<T>> toImmutableList() {
return upstream -> upstream.collect(ImmutableList::<T>builder, ImmutableList.Builder::add)
.map(ImmutableList.Builder::build)
.toFlowable(); *// must turn Single into Flowable*
}
}
你也应该能够将类似的转换应用到我们的joinToString()示例中的FlowableTransformer。
你可以考虑创建单独的静态实用类来分别存储你的 FlowableTransformers 和 ObservableTransformers,以防止名称冲突。我们的 FlowableTransformer 和 ObservableTransformer 的 toImmutableList() 变体不能存在于同一个静态实用类中,除非它们有不同的方法名称。但将它们放在单独的类中可能更干净,例如 MyObservableTransformers 和 MyFlowableTransformers。你也可以将它们放在具有相同类名 MyTransformers 的单独包中,一个用于 Observables,另一个用于 Flowables。
避免与 Transformers 共享状态
当你开始创建自己的 Transformers 和自定义操作符(稍后介绍)时,一个容易犯的错误是在多个订阅之间共享状态。这可能会迅速产生不希望出现的副作用和有缺陷的应用程序,这也是你创建自己的操作符时必须谨慎行事的原因之一。
假设你想创建一个 ObservableTransformer<T,IndexedValue<T>>,它将每个发射项与其连续的索引(从 0 开始)配对。首先,你创建一个 IndexedValue<T> 类来简单地将每个 T 值与一个 int index 配对:
static final class IndexedValue<T> {
final int index;
final T value;
IndexedValue(int index, T value) {
this.index = index;
this.value = value;
}
@Override
public String toString() {
return index + " - " + value;
}
}
然后,你创建一个 ObservableTransformer<T,IndexedValue<T>>,它使用 AtomicInteger 来递增并将一个整数附加到每个发射项。但我们的实现中存在一些问题:
static <T> ObservableTransformer<T,IndexedValue<T>> withIndex() {
final AtomicInteger indexer = new AtomicInteger(-1);
return upstream -> upstream.map(v -> new IndexedValue<T>(indexer.incrementAndGet(), v));
}
看到什么问题了吗?尝试运行这个具有两个观察者并使用此 withIndex() Transformers 的 Observable 操作。仔细查看输出:
import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
import java.util.concurrent.atomic.AtomicInteger;
public class Launcher {
public static void main(String[] args) {
Observable<IndexedValue<String>> indexedStrings =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.compose(withIndex());
indexedStrings.subscribe(v -> System.out.println("Subscriber 1: " + v));
indexedStrings.subscribe(v -> System.out.println("Subscriber 2: " + v));
}
static <T> ObservableTransformer<T,IndexedValue<T>> withIndex() {
final AtomicInteger indexer = new AtomicInteger(-1);
return upstream -> upstream.map(v -> new IndexedValue<T>(indexer.incrementAndGet(), v));
}
static final class IndexedValue<T> {
final int index;
final T value;
IndexedValue(int index, T value) {
this.index = index;
this.value = value;
}
@Override
public String toString() {
return index + " - " + value;
}
}
}
输出如下:
Subscriber 1: 0 - Alpha
Subscriber 1: 1 - Beta
Subscriber 1: 2 - Gamma
Subscriber 1: 3 - Delta
Subscriber 1: 4 - Epsilon
Subscriber 2: 5 - Alpha
Subscriber 2: 6 - Beta
Subscriber 2: 7 - Gamma
Subscriber 2: 8 - Delta
Subscriber 2: 9 - Epsilon
注意,AtomicInteger 的单个实例在两个订阅之间共享,这意味着其状态也被共享。在第二个订阅中,它不会从 0 开始,而是从上一个订阅留下的索引处开始,由于上一个订阅在 4 结束,所以从索引 5 开始。
除非你故意实现某些有状态的行为,否则这可能是可能产生令人沮丧的错误的不希望出现的副作用。常量通常没问题,但订阅之间的可变共享状态通常是你要避免的。
为每个订阅创建一个新的资源(如 AtomicInteger)的快速简单方法是将一切包裹在 Observable.defer() 中,包括 AtomicInteger 实例。这样,每次都会创建一个新的 AtomicInteger,并返回索引操作:
static <T> ObservableTransformer<T,IndexedValue<T>> withIndex() {
return upstream -> Observable.defer(() -> {
AtomicInteger indexer = new AtomicInteger(-1);
return upstream.map(v -> new IndexedValue<T>(indexer.incrementAndGet(), v));
});
}
你也可以在 Observable.fromCallable() 中创建一个 AtomicInteger,并使用 flatMap() 在它上面创建一个使用它的 Observable。
在这个特定的例子中,你也可以使用 Observable.zip() 或 zipWith() 与 Observable.range()。由于这也是一个纯 Rx 方法,多个订阅者之间不会共享状态,这也会解决我们的问题:
static <T> ObservableTransformer<T,IndexedValue<T>> withIndex() {
return upstream ->
Observable.zip(upstream,
Observable.range(0,Integer.MAX_VALUE),
(v,i) -> new IndexedValue<T>(i, v)
);
}
再次强调,在 Rx 中,意外的共享状态和副作用是危险的!无论你使用什么实现来创建你的 Transformer,如果可能的话,最好在你的实现中依赖纯 Rx 工厂和操作符。除非你满足某些奇怪的业务需求,其中明确需要共享状态,否则请避免创建可能被订阅共享的状态和对象。
使用 to() 进行流畅转换
在罕见的情况下,你可能发现自己需要将一个 Observable 传递给另一个 API,该 API 将其转换为专有类型。这可以通过将 Observable 作为参数传递给执行此转换的工厂来完成。然而,这并不总是感觉流畅,这就是 to() 操作符发挥作用的地方。
例如,JavaFX 有一个 Binding<T> 类型,它包含一个可变值,类型为 T,并且当它发生变化时,会通知受影响的用户界面元素进行更新。RxJavaFX 有 JavaFxObserver.toBinding() 和 JavaFxSubscriber.toBinding() 工厂,可以将 Observable<T> 或 Flowable<T> 转换为 JavaFX 的 Binding<T>。以下是一个简单的 JavaFX Application,它使用基于 Observable<String> 构建的 Binding<String>,该 Binding<String> 用于绑定到 label 的 textProperty() 操作符:
import io.reactivex.Observable;
import io.reactivex.rxjavafx.observers.JavaFxObserver;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.concurrent.TimeUnit;
public final class JavaFxApp extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
Label label = new Label("");
*// Observable with second timer*
Observable<String> seconds =
Observable.interval(1, TimeUnit.SECONDS)
.map(i -> i.toString())
.observeOn(JavaFxScheduler.platform());
*// Turn Observable into Binding*
Binding<String> binding = JavaFxObserver.toBinding(seconds);
*//Bind Label to Binding*
label.textProperty().bind(binding);
root.setMinSize(200, 100);
root.getChildren().addAll(label);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
由于我们已经习惯了使用 RxJava 进行流畅编程,将 Observable<String> 转换为 Binding<String> 的过程也变成 Observable 链的一部分不是很好吗?这样,我们就不必打破我们的流畅风格,也不必保存中间变量。这可以通过 to() 操作符来完成,它简单地接受一个 Function<Observable<T>,R> 来将 Observable<T> 转换为任何任意的 R 类型。在这种情况下,我们可以在 Observable 链的末尾使用 to() 将我们的 Observable<String> 转换为 Binding<String>:
import io.reactivex.Observable;
import io.reactivex.rxjavafx.observers.JavaFxObserver;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.concurrent.TimeUnit;
public final class JavaFxApp extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
Label label = new Label("");
*// Turn Observable into Binding*
Binding<String> binding =
Observable.interval(1, TimeUnit.SECONDS)
.map(i -> i.toString())
.observeOn(JavaFxScheduler.platform())
.to(JavaFxObserver::toBinding);
*//Bind Label to Binding*
label.textProperty().bind(binding);
root.setMinSize(200, 100);
root.getChildren().addAll(label);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
简单但很有帮助,对吧?当你处理基于 Rx Observables 和 Flowables 构建的专有非 Rx 类型时,这是一个方便的实用工具,可以保持流畅的 Rx 风格,尤其是在与绑定框架交互时。
操作符
理想情况下,你很少需要从头开始实现 ObservableOperator 或 FlowableOperator 来构建自己的操作符。ObservableTransformer 和 FlowableTransformer 希望能满足大多数可以使用现有操作符组合新操作符的情况,这通常是 safest 的途径。但有时,你可能发现自己必须做一些现有操作符无法做到或不方便做到的事情。在你用尽所有其他选项之后,你可能不得不创建一个操作符,该操作符在上游和下游之间操作每个 onNext()、onComplete() 和 onError() 事件。
在你出去创建自己的操作符之前,先尝试使用现有的操作符,通过 compose() 和转换器来实现。如果这还失败了,建议你在 StackOverflow 上发帖询问,看看 RxJava 社区是否存在这样的操作符或者是否可以轻松组合。RxJava 社区在 StackOverflow 上非常活跃,他们可能会提供一个解决方案,并且只在需要时增加解决方案的复杂性。
注意,David Karnok 的 RxJava2Extensions 和 Dave Moten 的 RxJava2-Extras 包含许多有用的转换器和操作符,可以增强 RxJava。你应该查看这些库,看看它们是否满足你的需求。
如果确定没有现有的解决方案,那么请谨慎地构建自己的操作符。再次强调,建议你首先在 StackOverflow 上寻求帮助。构建一个原生操作符并不容易,从 Rx 专家那里获得见解和经验是非常有价值的,并且很可能是有必要的。
实现 ObservableOperator
实现 ObservableOperator(以及 FlowableTransformer)比创建 ObservableTransformer 更复杂。不是通过组合一系列现有的操作符,而是通过实现自己的 Observer 来拦截来自上游的 onNext()、onComplete()、onError() 和 onSubscribe() 调用。这个 Observer 将逻辑上传递 onNext()、onComplete() 和 onError() 事件到下游 Observer,以实现所需操作。
假设你想创建自己的 doOnEmpty() 操作符,该操作符在调用 onComplete() 且没有发生任何发射时执行一个 Action。要创建自己的 ObservableOperator<Downstream,Upstream>(其中 Upstream 是上游发射类型,Downstream 是下游发射类型),你需要实现其 apply() 方法。这个方法接受一个 Observer<Downstream> 参数 observer 并返回一个 Observer<Upstream>。
你可以通过在 Observable 链中的 lift() 操作符中调用它来使用这个 ObservableOperator,如下所示:
import io.reactivex.Observable;
import io.reactivex.ObservableOperator;
import io.reactivex.Observer;
import io.reactivex.functions.Action;
import io.reactivex.observers.DisposableObserver;
public class Launcher {
public static void main(String[] args) {
Observable.range(1, 5)
.lift(doOnEmpty(() -> System.out.println("Operation 1 Empty!")))
.subscribe(v -> System.out.println("Operation 1: " + v));
Observable.<Integer>empty()
.lift(doOnEmpty(() -> System.out.println("Operation 2 Empty!")))
.subscribe(v -> System.out.println("Operation 2: " + v));
}
public static <T> ObservableOperator<T,T> doOnEmpty(Action action) {
return new ObservableOperator<T, T>() {
@Override
public Observer<? super T> apply(Observer<? super T> observer) throws Exception {
return new DisposableObserver<T>() {
boolean isEmpty = true;
@Override
public void onNext(T value) {
isEmpty = false;
observer.onNext(value);
}
@Override
public void onError(Throwable t) {
observer.onError(t);
}
@Override
public void onComplete() {
if (isEmpty) {
try {
action.run();
} catch (Exception e) {
onError(e);
return;
}
}
observer.onComplete();
}
};
}
};
}
}
输出如下:
Operation 1: 1
Operation 1: 2
Operation 1: 3
Operation 1: 4
Operation 1: 5
Operation 2 Empty!
在 apply() 中,你接收传递的 Observer,该 Observer 接受下游的事件。你创建另一个 Observer(在这种情况下,我们应该使用一个 DisposableObserver,它可以为我们处理销毁请求)来接收上游的发射和事件,并将它们传递给下游 Observer。你可以操纵事件以执行所需的逻辑,以及添加任何副作用。
在这种情况下,我们只是将上游的事件原封不动地传递到下游,但会跟踪是否调用了onNext()以标记是否有发射发生。当调用onComplete()且没有发射发生时,它将在onComplete()中执行用户指定的操作。通常,将可能抛出运行时错误的任何代码包裹在try-catch中,并将捕获的错误传递给onError()是一个好主意。
使用ObservableOperator时,你可能觉得奇怪,你得到的是下游作为输入,而必须为上游生成一个Observer作为输出。例如,在使用map()操作符时,函数接收上游的值并返回要向下游发射的值。这是因为来自ObservableOperator的代码在订阅时执行,此时调用是从末端的Observer(下游)向源Observable(上游)传递。
由于它是一个单抽象方法类,你也可以将你的ObservableOperator实现表达为一个 lambda,如下所示:
public static <T> ObservableOperator<T,T> doOnEmpty(Action action) {
return observer -> new DisposableObserver<T>() {
boolean isEmpty = true;
@Override
public void onNext(T value) {
isEmpty = false;
observer.onNext(value);
}
@Override
public void onError(Throwable t) {
observer.onError(t);
}
@Override
public void onComplete() {
if (isEmpty) {
try {
action.run();
} catch (Exception e) {
onError(e);
return;
}
}
observer.onComplete();
}
};
}
就像Transformers一样,在创建自定义操作符时要小心,除非你确实想要这样做,否则不要在订阅之间共享状态。这是一个相对简单的操作符,因为它是一个简单的响应式构建块,但操作符可以变得极其复杂。这尤其适用于操作符处理并发(例如,observeOn()和subscribeOn())或订阅之间共享状态(例如,replay())时。groupBy()、flatMap()和window()的实现同样复杂和错综。
在调用三个事件时,Observable契约中有一些规则你必须遵守。在onError()发生之后(或反之)永远不要调用onComplete()。在调用onComplete()或onError()之后,不要调用onNext(),并且在销毁之后不要调用任何事件。违反这些规则可能会产生意外的后果。
另一点需要指出的是,onNext()、onComplete()和onError()调用可以根据需要被操作和混合。例如,toList()不会为从上游接收到的每个onNext()调用都向下游传递一个onNext()调用。它将在内部列表中持续收集这些发射。当上游调用onComplete()时,它将在调用onComplete()之前在下游上调用onNext()以传递该列表。在这里,我们实现了自己的myToList()操作符来理解toList()是如何工作的,尽管在正常情况下,我们应该使用collect()或toList():
import io.reactivex.Observable;
import io.reactivex.ObservableOperator;
import io.reactivex.observers.DisposableObserver;
import java.util.ArrayList;
import java.util.List;
public class Launcher {
public static void main(String[] args) {
Observable.range(1, 5)
.lift(myToList())
.subscribe(v -> System.out.println("Operation 1: " + v));
Observable.<Integer>empty()
.lift(myToList())
.subscribe(v -> System.out.println("Operation 2: " + v));
}
public static <T> ObservableOperator<List<T>,T> myToList() {
return observer -> new DisposableObserver<T>() {
ArrayList<T> list = new ArrayList<>();
@Override
public void onNext(T value) {
*//add to List, but don't pass anything downstream*
list.add(value);
}
@Override
public void onError(Throwable t) {
observer.onError(t);
}
@Override
public void onComplete() {
observer.onNext(list); *//push List downstream*
observer.onComplete();
}
};
}
}
输出如下:
Operation 1: [1, 2, 3, 4, 5]
Operation 2: []
在你开始雄心勃勃地创建自己的操作符之前,研究 RxJava 或其他库(如 RxJava2-Extras)的源代码可能是个好主意。操作符的正确实现可能很困难,因为你需要很好地理解如何从命令式模式构建响应式模式。你也会想彻底测试它(我们将在第十章 Testing and Debugging 中介绍),以确保在生产之前它表现正确。
FlowableOperator
当你创建自己的ObservableOperator时,你很可能会同时创建一个FlowableOperator的对应物。这样,你的操作符就可以用于 Observables 和 Flowables。幸运的是,FlowableOperator的实现方式与ObservableOperator类似,如下所示:
import io.reactivex.Flowable;
import io.reactivex.FlowableOperator;
import io.reactivex.functions.Action;
import io.reactivex.subscribers.DisposableSubscriber;
import org.reactivestreams.Subscriber;
public class Launcher {
public static void main(String[] args) {
Flowable.range(1, 5)
.lift(doOnEmpty(() -> System.out.println("Operation 1 Empty!")))
.subscribe(v -> System.out.println("Operation 1: " + v));
Flowable.<Integer>empty()
.lift(doOnEmpty(() -> System.out.println("Operation 2 Empty!")))
.subscribe(v -> System.out.println("Operation 2: " + v));
}
public static <T> FlowableOperator<T,T> doOnEmpty(Action action) {
return new FlowableOperator<T, T>() {
@Override
public Subscriber<? super T> apply(Subscriber<? super T> subscriber) throws Exception {
return new DisposableSubscriber<T>() {
boolean isEmpty = true;
@Override
public void onNext(T value) {
isEmpty = false;
subscriber.onNext(value);
}
@Override
public void onError(Throwable t) {
subscriber.onError(t);
}
@Override
public void onComplete() {
if (isEmpty) {
try {
action.run();
} catch (Exception e) {
onError(e);
return;
}
}
subscriber.onComplete();
}
};
}
};
}
}
我们没有使用观察者,而是使用了订阅者,希望这一点不会让你感到惊讶。通过apply()传递的Subscriber接收下游的事件,而实现的Subscriber接收上游的事件,并将其转发到下游(就像我们使用了DisposableObserver一样,我们使用DisposableSubscriber来处理销毁/取消订阅)。就像之前一样,onComplete()将验证没有发生发射,并在这种情况下运行指定的操作。
当然,你也可以将你的FlowableOperator表达为 lambda 表达式:
public static <T> FlowableOperator<T,T> doOnEmpty(Action action) {
return subscriber -> new DisposableSubscriber<T>() {
boolean isEmpty = true;
@Override
public void onNext(T value) {
isEmpty = false;
subscriber.onNext(value);
}
@Override
public void onError(Throwable t) {
subscriber.onError(t);
}
@Override
public void onComplete() {
if (isEmpty) {
try {
action.run();
} catch (Exception e) {
onError(e);
return;
}
}
subscriber.onComplete();
}
};
}
再次强调,当你开始实现自己的操作符时,要勤奋并彻底,尤其是在它们达到复杂性的阈值时。努力使用现有的操作符来组合 Transformers,并在 StackOverflow 或 RxJava 社区中寻求帮助,看看其他人是否能首先指出明显的解决方案。实现操作符是一件你应该谨慎对待的事情,只有在所有其他选项都已用尽时才追求。
单独为 Singles、Maybes 和 Completables 创建的 Transformer 和操作符
对于Single、Maybe和Completable,都有对应的 Transformer 和操作符。当你想要创建一个产生Single的Observable或Flowable操作符时,你可能发现通过调用其toObservable()或toFlowable()操作符将其转换回Observable/Flowable可能更容易。这也适用于Maybe。
如果在某些罕见的情况下,你需要创建一个专门用于将Single转换为另一个Single的Transformer或操作符,你将想要使用SingleTransformer或SingleOperator。Maybe和Completable将分别有MaybeTransformer/MaybeOperator和CompletableTransformer/CompletableOperator的对应物。所有这些apply()的实现应该有类似的体验,你将使用SingleObserver、MaybeObserver和CompletableObserver来代理上游和下游。
这里是一个SingleTransformer的示例,它接受Single<Collection<T>>并将发射的Collection映射到一个不可修改的集合:
import io.reactivex.Observable;
import io.reactivex.SingleTransformer;
import java.util.Collection;
import java.util.Collections;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha","Beta","Gamma","Delta","Epsilon")
.toList()
.compose(toUnmodifiable())
.subscribe(System.out::println);
}
public static <T> SingleTransformer<Collection<T>, Collection<T>> toUnmodifiable() {
return singleObserver -> singleObserver.map(Collections::unmodifiableCollection);
}
}
输出如下:
[Alpha, Beta, Gamma, Delta, Epsilon]
使用 RxJava2-Extras 和 RxJava2Extensions
如果你感兴趣想要了解 RxJava 提供的额外操作符,那么探索RxJava2-Extras和RxJava2Extensions库可能是有价值的。虽然这两个库都没有达到 1.0 版本,但有用的操作符、转换器和Observable/Flowable工厂作为持续项目不断添加。
两个有用的操作符是toListWhile()和collectWhile()。这些操作符会在满足一定条件时将发射项缓冲到列表或集合中。由于BiPredicate将列表/集合和下一个T项作为 lambda 输入参数传递,你可以使用这个功能来缓冲项目,但在发射项发生变化时停止缓冲。在这里,我们持续将字符串收集到列表中,但在长度变化时将列表向前推进(类似于distinctUntilChanged())。我们还将检查列表是否为空,因为这是下一个缓冲的开始,以及从列表中采样一个项目来与下一个发射项比较长度:
import com.github.davidmoten.rx2.flowable.Transformers;
import io.reactivex.Flowable;
public class Launcher {
public static void main(String[] args) {
Flowable.just("Alpha","Beta","Zeta","Gamma","Delta","Theta","Epsilon")
.compose(Transformers.toListWhile((list,next) ->
list.size() == 0 || list.get(0).length() == next.length()
)).subscribe(System.out::println);
}
}
输出如下:
[Alpha]
[Beta, Zeta]
[Gamma, Delta, Theta]
[Epsilon]
在 RxJava2-Extras 和 RxJava2Extensions 上花费一些高质量的时间来了解它们的自定义操作符。这样,你就不必重新发明可能已经存在的东西,而且已经有很多强大的工厂和操作符。我个人最喜欢的一个是可重置的cache()操作符,它的工作方式类似于我们在第五章中学习的缓存,多播,但它可以在任何时间清除并重新订阅到源。它还可以在固定的时间间隔或无活动期间清除缓存,防止过时的缓存持久化。
摘要
在本章中,我们通过创建自己的操作符来入门。使用ObservableTransformer和FlowableTransformer将现有操作符组合在一起以创建新的操作符更为可取,即使如此,引入可能导致不希望副作用的状态资源时也需要谨慎。当所有其他方法都失败时,你可以创建自己的ObservableOperator或FlowableOperator,并在低级别创建一个拦截和转发每个发射项和事件的操作符。这可能很棘手,你应该尝试所有其他选项,但通过仔细研究和测试,创建操作符可以是一项有价值的先进技能。只是要小心不要重复造轮子,并在开始尝试自定义操作符时寻求 Rx 社区的帮助。
如果你真正对实现自己的操作符(在低级别上,而不是使用 Transformers)感兴趣,那么一定要研究 RxJava 和其他信誉良好的 RxJava 扩展库中的现有操作符。很容易拼凑出一个操作符并相信一切都会顺利进行,但实际上有很多你可能忽略的复杂性。你的操作符需要支持序列化、可取消、并发,并处理重入性(当发射操作在同一个线程上引发请求时发生)。当然,有些操作符比其他操作符简单,但你绝不应该在没有深入研究之前就做出假设。
在下一章中,我们将学习关于针对 RxJava API 和实用工具进行单元测试的不同策略。无论你是否创建了自己的自定义操作符,或者在工作中有一个 Rx 项目,自动化测试都是你想要熟练掌握的技能。我们还将学习如何调试 RxJava 应用程序,这并不总是容易,但可以有效地完成。
第十章:测试和调试
虽然单元测试并不是确保您的代码正确工作的银弹,但追求它是良好的实践。这对于您的逻辑高度确定性和足够模块化以隔离的情况尤其正确。
初看起来,使用 RxJava 进行测试可能并不直接。毕竟,RxJava 声明的是行为而不是状态。那么我们如何测试行为是否正确工作,尤其是在大多数测试框架期望有状态结果的情况下呢?幸运的是,RxJava 提供了几个辅助测试的工具,您可以使用这些工具与您喜欢的测试框架一起使用。市场上有很多可以与 RxJava 一起工作的测试工具,但本章我们将使用 JUnit。
我们还将介绍一些调试 RxJava 程序的技巧。RxJava 的一个缺点是,当出现错误时,传统的调试方法并不总是有效的,尤其是因为堆栈跟踪并不总是有帮助,断点也不容易应用。但是,RxJava 在调试方面提供了一个好处:通过正确的方法,您可以遍历整个响应式链,找到导致问题发生的算子。问题变得非常线性,变成隔离坏链的问题。这可以显著简化调试过程。
本章将涵盖许多测试功能,因此我们将从简单的直观方法开始,以涵盖基本的阻塞算子。然后,我们将升级到更健壮的工具,例如 TestObserver、TestSubscriber 和 TestScheduler,您可能会在您的应用程序中使用这些工具。
在本章中,我们将涵盖以下主题:
-
blockingSubscribe() -
阻塞算子
-
TestObserver和TestSubscriber -
TestScheduler -
RxJava 调试策略
配置 JUnit
在本节中,我们将使用 JUnit 作为我们的测试框架。请将以下依赖项添加到您的 Maven 或 Gradle 项目中。
这里是 Maven 的配置:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
这里是 Gradle 的配置:
dependencies {
compile 'junit:junit:4.12'
}
为了节省您的时间,组织您的代码项目以符合 Maven 标准目录布局。您可能希望将测试类放在 /src/test/java/ 文件夹中,这样 Maven 和 Gradle 将自动将其识别为测试代码文件夹。您还应该在项目的 /src/main/java/ 文件夹中放置您的生产代码。您可以在 maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html 上了解更多关于 Maven 标准目录布局的信息。
阻塞订阅者
记得有时候我们不得不阻止主线程从不同的线程上跳过Observable或Flowable操作,并防止它在有机会触发之前退出应用程序吗?我们经常使用Thread.sleep()来防止这种情况,尤其是在我们使用Observable.interval()、subscribeOn()或observeOn()时。以下代码展示了我们通常是如何做到这一点的,并使一个Observable.interval()应用程序保持活跃五秒钟:
import io.reactivex.Observable;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Observable.interval(1, TimeUnit.SECONDS)
.take(5)
.subscribe(System.out::println);
sleep(5000);
}
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当涉及到单元测试时,单元测试通常必须在开始下一个测试之前完成。当我们有一个在另一个线程上发生的Observable或Flowable操作时,这可能会变得相当混乱。当一个test方法声明了一个异步的Observable或Flowable链操作时,我们需要阻塞并等待该操作完成。
在这里,我们创建了一个测试来确保从Observable.interval()发出五个发射,并在验证它被增加了五次之前增加AtomicInteger:
import io.reactivex.Observable;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testBlockingSubscribe() {
AtomicInteger hitCount = new AtomicInteger();
Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS)
.take(5);
source.subscribe(i -> hitCount.incrementAndGet());
assertTrue(hitCount.get() == 5);
}
}
我们使用@Test注解来告诉JUnit这是一个测试方法。您可以通过在 IntelliJ IDEA 的 gutter 中点击其绿色的三角形播放按钮或在 Gradle 或 Maven 中运行测试任务来运行它。
然而,存在一个问题。当你运行这个测试时,断言失败。Observable.interval()正在计算线程上运行,而主线程在五个发射触发之前就冲过去了。主线程在五个发射触发之前执行assertTrue(),因此发现hitCount是0而不是5。我们需要停止主线程,直到subscribe()完成并调用onComplete()。
幸运的是,我们不需要使用同步器和其他原生 Java 并发工具来发挥创意。相反,我们可以使用blockingSubscribe(),它将阻塞声明的主线程,直到onComplete()(或onError())被调用。一旦收集到这五个发射,主线程就可以继续进行,并成功执行断言,就像这里所展示的。然后测试应该通过:
import io.reactivex.Observable;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testBlockingSubscribe() {
AtomicInteger hitCount = new AtomicInteger();
Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS)
.take(5);
source.blockingSubscribe(i -> hitCount.incrementAndGet());
assertTrue(hitCount.get() == 5);
}
}
正如我们将在本章中看到的,除了blockingSubscribe()之外,还有更好的测试方法。但blockingSubscribe()是一个快速而有效的方法来停止声明线程,并在继续之前等待Observable或Flowable完成,即使它是在不同的线程上。只需确保源在某一点终止,否则测试将永远不会完成。
在测试之外以及在生产环境中使用blockingSubscribe()时,要谨慎。确实有在接口非响应式 API 时它是合法解决方案的时候。例如,在生产环境中使用它来无限期地保持应用程序活跃,并且是使用Thread.sleep()的有效替代方案。只是要小心确保 RxJava 的异步优势不被削弱。
阻塞算子
在 RxJava 中,有一组我们尚未介绍的称为阻塞操作符的操作符。这些操作符充当反应式世界和有状态世界之间的即时代理,阻塞并等待结果发出,但以非反应式的方式返回。即使反应式操作在不同的线程上运行,阻塞操作符也会停止声明线程并使其以同步方式等待结果,就像blockingSubscribe()一样。
阻塞操作符在使Observable或Flowable的结果易于评估方面特别有用。然而,你希望在生产环境中避免使用它们,因为它们会鼓励反模式并损害响应式编程的好处。对于测试,你仍然希望优先考虑TestObserver和TestSubscriber,我们将在后面介绍。但如果你确实需要它们,这里有一些阻塞操作符。
blockingFirst()
blockingFirst()操作符将停止调用线程并使其等待第一个值被发射并返回(即使链在具有observeOn()和subscribeOn()的不同线程上运行)。假设我们想要测试一个Observable链,该链仅过滤长度为四的字符串发射序列。如果我们想要断言第一个通过此操作的发射是Beta,我们可以这样测试:
import io.reactivex.Observable;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testFirst() {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Zeta");
String firstWithLengthFour = source.filter(s -> s.length() == 4)
.blockingFirst();
assertTrue(firstWithLengthFour.equals("Beta"));
}
}
在这里,我们的单元测试称为testFirst(),它将断言第一个长度为四的字符串是Beta。请注意,我们不是使用subscribe()或blockingSubscribe()来接收发射,而是使用blockingFirst(),它将以非反应式的方式返回第一个发射。换句话说,它返回一个简单的字符串,而不是发出字符串的Observable。
这将阻塞声明线程,直到返回值并分配给firstWithLengthFour。然后我们使用这个保存的值来断言它实际上是Beta。
看到 blockingFirst(),你可能想在生产代码中使用它来保存结果状态并稍后引用它。尽量不要这样做!虽然在某些情况下你可能会找到合理的理由(例如,将发射保存到HashMap中进行昂贵的计算和查找),但阻塞操作符很容易被滥用。如果你需要持久化值,尽量使用replay()和其他反应式缓存策略,这样你就可以轻松地更改其行为和并发策略。阻塞通常会使得你的代码更缺乏灵活性,并损害 Rx 的好处。
注意,如果没有任何发射通过,blockingFirst()操作符将抛出错误并使测试失败。但是,你可以提供一个默认值作为blockingFirst()的重载,这样它总是有一个回退值。
与blockingFirst()类似的阻塞操作符是blockingSingle(),它期望只发射一个项目,但如果有多于一个项目,则抛出错误。
blockingGet()
Maybe 和 Single 没有提供 blockingFirst(),因为最多只能有一个元素。从逻辑上讲,对于 Single 和 Maybe,这并不是确切的第一元素,而是唯一的元素,所以等效的算子是 blockingGet()。
在这里,我们断言长度为四的所有项只包含 Beta 和 Zeta,并且我们使用 toList() 收集它们,这会产生一个 Single<List<String>>。我们可以使用 blockingGet() 等待这个列表,并断言它等于我们期望的结果:
import io.reactivex.Observable;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testSingle() {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Zeta");
List<String> allWithLengthFour = source.filter(s -> s.length() == 4)
.toList()
.blockingGet();
assertTrue(allWithLengthFour.equals(Arrays.asList("Beta","Zeta")));
}
}
blockingLast()
如果有 blockingFirst(),那么只有 blockingLast() 才有意义。这将阻塞并返回从 Observable 或 Flowable 操作中发出的最后一个值。当然,它不会返回任何内容,直到 onComplete() 被调用,所以这是你想要避免与无限源一起使用的情况。
在这里,我们断言从我们的操作中发出的最后一个四字符字符串是 Zeta:
import io.reactivex.Observable;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testLast() {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Zeta");
String lastWithLengthFour = source.filter(s -> s.length() == 4)
.blockingLast();
assertTrue(lastWithLengthFour.equals("Zeta"));
}
}
就像 blockingFirst() 一样,如果没有任何排放发生,blockingLast() 将会抛出一个错误,但你可以为默认值指定一个重载。
blockingIterable()
最有趣的阻塞算子之一是 blockingIterable()。与之前的示例不同,它不会返回单个排放,而是会通过 iterable<T> 提供排放。Iterable<T> 提供的 Iterator<T> 将会阻塞迭代线程,直到下一个排放可用,迭代将在 onComplete() 被调用时结束。在这里,我们遍历每个返回的字符串值,以确保其实际长度是 5:
import io.reactivex.Observable;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testIterable() {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Zeta");
Iterable<String> allWithLengthFive = source.filter(s -> s.length() == 5)
.blockingIterable();
for (String s: allWithLengthFive) {
assertTrue(s.length() == 5);
}
}
}
blockingIterable() 将会排队等待未消费的值,直到 Iterator 能够处理它们。在没有背压的情况下,这可能会出现问题,因为你可能会遇到 OutOfMemoryException 错误。
与 C# 不同,请注意 Java 的 for-each 构造不会处理取消、中断或处置。你可以通过在 try-finally 中迭代可迭代的 Iterator 来解决这个问题。在 finally 块中,将 Iterator 强制转换为 disposable,这样你就可以调用它的 dispose() 方法。
blockingIterable() 可以帮助快速将 Observable 或 Flowable 转换为拉取驱动的函数序列类型,例如 Java 8 Stream 或 Kotlin 序列,这些可以基于可迭代对象构建。然而,对于 Java 8 streams,你可能会更倾向于使用 David Karnok 的 RxJava2Jdk8Interop 库 (github.com/akarnokd/RxJava2Jdk8Interop),这样终止处理会更加安全。
blockingForEach()
我们可以以更流畅的方式为每个任务执行阻塞,即使用blockingForEach()操作符而不是blockingIterable()。这将阻塞声明线程,等待每个发射被处理后再允许线程继续。我们可以简化之前的示例,其中我们迭代每个发射的字符串并确保其长度为五,并将断言作为 lambda 表达式指定在forEach()操作符中:
import io.reactivex.Observable;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class RxTest {
@Test
public void testBlockingForEach() {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Zeta");
source.filter(s -> s.length() == 5)
.blockingForEach(s -> assertTrue(s.length() == 5));
}
}
blockingForEach()的一个变体是blockingForEachWhile(),它接受一个谓词,如果谓词对发射评估为假,则优雅地终止序列。如果所有发射都不将被消费,并且你希望优雅地终止,这可能是有用的。
blockingNext()
blockingNext()将返回一个可迭代对象,并阻塞每个迭代器的next()请求,直到下一个值被提供。在最后一个满足的next()请求之后和当前next()之前发生的发射将被忽略。在这里,我们有一个每微秒(千分之一毫秒)发射的源。请注意,blockingNext()返回的可迭代对象忽略了它之前错过的值:
import io.reactivex.Observable;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
public class RxTest {
@Test
public void testBlockingNext() {
Observable<Long> source =
Observable.interval(1, TimeUnit.MICROSECONDS)
.take(1000);
Iterable<Long> iterable = source.blockingNext();
for (Long i: iterable) {
System.out.println(i);
}
}
}
输出如下:
0
6
9
11
17
23
26
blockingLatest()
与blockingLatest()相反,blockingLatest()的可迭代对象不会等待下一个值,而是请求最后一个发射的值。在此之前的任何未捕获的值都将被遗忘。如果迭代器的next()之前已经消费了最新的值,它将不会重新消费,并且会阻塞,直到下一个值到来:
import io.reactivex.Observable;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
public class RxTest {
@Test
public void testBlockingLatest() {
Observable<Long> source =
Observable.interval(1, TimeUnit.MICROSECONDS)
.take(1000);
Iterable<Long> iterable = source.blockingLatest();
for (Long i: iterable) {
System.out.println(i);
}
}
}
输出如下:
0
49
51
53
55
56
58
...
blockingMostRecent()
blockingMostRecent()与blockingLatest()类似,但它会在迭代器的每个next()调用中重复消费最新的值,即使它已经被消费过。它还需要一个defaultValue参数,以便在没有发射值时返回某个值。在这里,我们使用blockingMostRecent()对每 10 毫秒发射一次的Observable进行操作。默认值是-1,并且它会重复消费每个值,直到下一个值被提供:
import io.reactivex.Observable;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
public class RxTest {
@Test
public void testBlockingMostRecent() {
Observable<Long> source =
Observable.interval(10, TimeUnit.MILLISECONDS)
.take(5);
Iterable<Long> iterable = source.blockingMostRecent(-1L);
for (Long i: iterable) {
System.out.println(i);
}
}
}
输出如下:
-1
-1
-1
...
0
0
0
...
1
1
1
...
在我们完成对阻塞操作符的介绍后,应该再次强调,它们可以是一种有效的简单断言方式,并提供阻塞以获取结果的方法,以便它们可以轻松地被测试框架消费。然而,你应尽可能避免在生产中使用阻塞操作符。尽量不要屈服于便利的诱惑,因为你会发现它们可以迅速削弱响应式编程的灵活性和优势。
使用 TestObserver 和 TestSubscriber
到目前为止,我们已经在本章中介绍了 blockingSubscribe() 和几个阻塞操作符。虽然你可以使用这些阻塞工具来进行简单的断言,但测试反应式代码的更全面的方法不仅仅是阻塞一个或多个值。毕竟,我们应该做的不仅仅是测试 onNext() 调用。我们还有 onComplete() 和 onError() 事件需要考虑!此外,简化测试其他 RxJava 事件,如订阅、处置和取消,也会很棒。
所以,让我们来介绍 TestObserver 和 TestSubscriber,它们是你在测试 RxJava 应用程序时的两位最佳伙伴。
TestObserver 和 TestSubscriber 是一个方便的测试方法宝库,其中许多方法断言某些事件已经发生或接收到了特定的值。还有一些阻塞方法,例如 awaitTerminalEvent(),它将停止调用线程,直到反应式操作终止。
TestObserver 用于 Observable、Single、Maybe 和 Completable 源,而 TestSubscriber 用于 Flowable 源。以下是一个单元测试示例,展示了几个 TestObserver 方法,这些方法也存在于 TestSubscriber 上,如果你正在处理 Flowables。这些方法执行诸如断言某些事件是否发生(或未发生)、等待终止或断言接收到了特定值等任务:
import io.reactivex.Observable;
import io.reactivex.observers.TestObserver;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
public class RxTest {
@Test
public void usingTestObserver() {
*//An Observable with 5 one-second emissions*
Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS)
.take(5);
*//Declare TestObserver*
TestObserver<Long> testObserver = new TestObserver<>();
*//Assert no subscription has occurred yet*
testObserver.assertNotSubscribed();
*//Subscribe TestObserver to source*
source.subscribe(testObserver);
*//Assert TestObserver is subscribed*
testObserver.assertSubscribed();
*//Block and wait for Observable to terminate*
testObserver.awaitTerminalEvent();
*//Assert TestObserver called onComplete()*
testObserver.assertComplete();
*//Assert there were no errors*
testObserver.assertNoErrors();
*//Assert 5 values were received*
testObserver.assertValueCount(5);
*//Assert the received emissions were 0, 1, 2, 3, 4*
testObserver.assertValues(0L, 1L, 2L, 3L, 4L);
}
}
这只是众多测试方法中的一小部分,它们将使你的单元测试更加全面和流畅。大多数 TestObserver 方法返回 TestObserver,因此你可以流畅地链式调用这些断言(这也适用于 TestSubscriber)。
注意,awaitTerminalEvent() 操作符可以接受一个超时参数,如果在指定时间之前源没有完成,它将抛出一个错误。
花些时间熟悉所有这些测试方法,以便你了解你做出的不同断言。尽可能优先使用 TestObserver 和 TestSubscriber 而不是阻塞操作符。这样,你可以花更少的时间维护测试,并确保你覆盖了 Observable 或 Flowable 操作的生命周期中的所有事件范围。
TestObserver 实现了 Observer、MaybeObserver、SingleObserver 和 CompetableObserver,以支持所有这些反应式类型。如果你正在测试一个长时间运行的异步源,你可能想使用 awaitCount() 来等待至少一定数量的发射以进行断言,而不是等待 onComplete() 调用。
使用 TestScheduler 操作时间
在我们之前的例子中,您注意到测试一个时间驱动的 Observable 或 Flowable 需要时间流逝到测试完成吗?在上一个练习中,我们从每秒发射一次的 Observable.interval() 中获取了五次发射,因此该测试花费了 5 秒来完成。如果我们有很多处理时间驱动源的单元测试,测试完成可能需要很长时间。如果我们可以模拟时间流逝而不是实际体验它们,那不是很好吗?
TestScheduler 正好能做这件事。它是一个调度器实现,允许我们通过特定的已过时间量进行快进,并且我们可以在每次快进后进行任何断言,以查看发生了哪些事件。
在这里,我们创建了一个针对每分钟发射一次的 Observable.interval() 的测试,并最终断言在 90 分钟后发生了 90 次发射。我们不必在真实时间中等待整整 90 分钟,而是使用 TestObserver 人工地流逝这 90 分钟。这使得测试可以立即运行:
import io.reactivex.Observable;
import io.reactivex.observers.TestObserver;
import io.reactivex.schedulers.TestScheduler;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
public class RxTest {
@Test
public void usingTestScheduler() {
*//Declare TestScheduler*
TestScheduler testScheduler = new TestScheduler();
*//Declare TestObserver*
TestObserver<Long> testObserver = new TestObserver<>();
*//Declare Observable emitting every 1 minute*
Observable<Long> minuteTicker =
Observable.interval(1, TimeUnit.MINUTES, testScheduler);
*//Subscribe to TestObserver*
minuteTicker.subscribe(testObserver);
*//Fast forward by 30 seconds*
testScheduler.advanceTimeBy(30, TimeUnit.SECONDS);
*//Assert no emissions have occurred yet*
testObserver.assertValueCount(0);
*//Fast forward to 70 seconds after subscription*
testScheduler.advanceTimeTo(70, TimeUnit.SECONDS);
*//Assert the first emission has occurred*
testObserver.assertValueCount(1);
*//Fast Forward to 90 minutes after subscription*
testScheduler.advanceTimeTo(90, TimeUnit.MINUTES);
*//Assert 90 emissions have occurred*
testObserver.assertValueCount(90);
}
}
很酷,对吧?这几乎就像是时间旅行!我们将 Observable.interval() 放在 TestScheduler 上。这样,TestScheduler 控制了 Observable 如何解释时间并推动发射。我们使用 advanceTimeBy() 快进 30 秒,然后断言还没有发生任何发射。然后我们使用 advanceTimeTo() 跳转到订阅发生后的 70 秒,并断言确实发生了一次发射。最后,我们在订阅后快进 90 分钟,并断言确实发生了 90 次发射。
所有这些都在瞬间完成,而不是花费 90 分钟,这表明确实可以在不实际流逝该时间的情况下测试时间驱动的 Observable/Flowable 操作。请注意,advanceTimeBy() 将相对于当前时间快进指定的时间间隔,而 advanceTimeTo() 将跳转到自订阅发生以来的确切流逝时间。
总结来说,当您需要虚拟表示时间流逝时,请使用 TestScheduler,但请注意,它不是一个线程安全的调度器,不应与实际并发一起使用。一个常见的陷阱是使用许多操作符和调度器的复杂流程,这些调度器不易配置为使用 TestScheduler。在这种情况下,您可以使用 RxJavaPlugins.setComputationScheduler() 和类似方法来覆盖标准调度器,并在其中注入 TestScheduler。
在 TestScheduler 中还有两个其他方法需要注意。now() 将返回在您指定的单位中虚拟流逝了多少时间。triggerActions() 方法将启动任何计划触发但尚未虚拟流逝的动作。
调试 RxJava 代码
RxJava 初看起来并不容易调试,主要是因为缺乏调试工具和它可能产生的大量堆栈跟踪。有创建针对 RxJava 的有效调试工具的努力,最著名的是 Android 的 Frodo 库 (github.com/android10/frodo)。我们不会涵盖任何 RxJava 的调试工具,因为还没有标准化,但我们将了解一种有效的调试反应式代码的方法。
在调试 RxJava 操作中,一个常见的主题是找到导致问题的 Observable/Flowable 链中的坏链接或操作符。无论是否正在发射错误,onComplete() 永远没有被调用,或者 Observable 突然为空,你通常必须从链的起始处,即源头开始,然后验证每个下游步骤,直到找到不正常工作的那个。
嘿,我们有一个 Observable 正在推送包含数字和由斜杠 "/" 分隔的字母词的五个字符串。我们想要在斜杠 "/" 上分割这些字符串,只过滤字母词,并将它们捕获在 TestObserver 中。然而,运行这个操作,你会看到这个测试失败了:
import io.reactivex.observers.TestObserver;
import org.junit.Test;
import io.reactivex.Observable;
public class RxTest {
@Test
public void debugWalkthrough() {
*//Declare TestObserver*
TestObserver<String> testObserver = new TestObserver<>();
*//Source pushing three strings*
Observable<String> items =
Observable.just("521934/2342/Foxtrot",
"Bravo/12112/78886/Tango",
"283242/4542/Whiskey/2348562");
*//Split and concatMap() on "/"*
items.concatMap(s ->
Observable.fromArray(s.split("/"))
)
*//filter for only alphabetic Strings using regex*
.filter(s -> s.matches("[A-Z]+"))
*//Subscribe the TestObserver*
.subscribe(testObserver);
*//Why are no values being emitted?*
System.out.println(testObserver.values());
*//This fails due to no values*
testObserver.assertValues("Foxtrot","Bravo","Tango","Whiskey");
}
}
输出结果如下:
[]
java.lang.AssertionError: Value count differs; Expected: 4 [Foxtrot, Bravo, Tango, Whiskey],
Actual: 0 [] (latch = 0, values = 0, errors = 0, completions = 1)
at io.reactivex.observers.BaseTestConsumer.fail(BaseTestConsumer.java:163)
at io.reactivex.observers.BaseTestConsumer.assertValues(BaseTestConsumer.java:485)
at RxTest.debugWalkthrough(RxTest.java:32)
...
那么到底出了什么问题?我们如何调试这个失败的测试?嗯,记住 RxJava 操作是一个管道。正确的发射项应该流动并通过,到达 Observer。但是没有收到任何发射项。让我们穿上我们的管道工装备,找出管道中的堵塞在哪里。我们将从源头开始。
在源头之后和 concatMap() 之前立即放置 doOnNext(),并打印每个发射项。这让我们能够看到从源头 Observable 中出来的内容。如图所示,我们应该看到所有来自源头的发射项被打印出来,这表明没有发射项被遗漏,并且源头上游运行正常:
*//Split and concatMap() on "/"*
items.doOnNext(s -> System.out.println("Source pushed: " + s))
.concatMap(s ->
Observable.fromArray(s.split("/"))
)
输出结果如下:
Source pushed: 521934/2342/Foxtrot
Source pushed: Bravo/12112/78886/Tango
Source pushed: 283242/4542/Whiskey/2348562
[]
java.lang.AssertionError: Value count differs; Expected ...
让我们继续向下流,接下来看看 concatMap()。也许它遗漏了发射项,所以让我们检查一下。在 concatMap() 之后放置 doOnNext() 并打印每个发射项,以查看是否所有这些都能通过,如图所示:
*//Split and concatMap() on "/"*
items.concatMap(s ->
Observable.fromArray(s.split("/"))
)
.doOnNext(s -> System.out.println("concatMap() pushed: " + s))
输出结果如下:
concatMap() pushed: 521934
concatMap() pushed: 2342
concatMap() pushed: Foxtrot
concatMap() pushed: Bravo
concatMap() pushed: 12112
concatMap() pushed: 78886
concatMap() pushed: Tango
concatMap() pushed: 283242
concatMap() pushed: 4542
concatMap() pushed: Whiskey
concatMap() pushed: 2348562
[]
java.lang.AssertionError: Value count differs; Expected ...
好的,所以 concatMap() 运行正常,所有的发射项都通过了。所以 concatMap() 内部的分割操作没有问题。让我们继续向下流,在 filter() 之后放置 doOnNext()。如图所示,打印每个发射项以查看我们想要的项是否从 filter() 中出来:
*//filter for only alphabetic Strings using regex*
.filter(s -> s.matches("[A-Z]+"))
.doOnNext(s -> System.out.println("filter() pushed: " + s))
输出结果如下:
[]
java.lang.AssertionError: Value count differs; Expected ...
哎!在filter()之后没有打印出任何排放项,这意味着没有任何东西通过它。filter()是导致问题的操作符。我们原本打算过滤掉数字字符串,只发出字母词。但不知何故,所有排放项都被过滤掉了。如果你对正则表达式有所了解,请注意我们只对完全大写的字符串进行限定。实际上,我们还需要对小写字母进行限定,所以这里是我们需要的修正:
*//filter for only alphabetic Strings using regex*
.filter(s -> s.matches("[A-Za-z]+"))
.doOnNext(s -> System.out.println("filter() pushed: " + s))
输出结果如下:
filter() pushed: Foxtrot
filter() pushed: Bravo
filter() pushed: Tango
filter() pushed: Whiskey
[Foxtrot, Bravo, Tango, Whiskey]
好的,它已经修复了!我们的单元测试最终通过了,下面是它的全部内容。现在问题已经解决,我们完成了调试,我们可以移除doOnNext()和任何打印调用:
import io.reactivex.observers.TestObserver;
import org.junit.Test;
import io.reactivex.Observable;
public class RxTest {
@Test
public void debugWalkthrough() {
*//Declare TestObserver*
TestObserver<String> testObserver = new TestObserver<>();
*//Source pushing three strings*
Observable<String> items =
Observable.just("521934/2342/Foxtrot",
"Bravo/12112/78886/Tango",
"283242/4542/Whiskey/2348562");
*//Split and concatMap() on "/"*
items.concatMap(s ->
Observable.fromArray(s.split("/"))
)
*//filter for only alphabetic Strings using regex*
.filter(s -> s.matches("[A-Za-z]+"))
*//Subscribe the TestObserver*
.subscribe(testObserver);
*//This succeeds*
testObserver.assertValues("Foxtrot","Bravo","Tango","Whiskey");
}
}
输出结果如下:
[Foxtrot, Bravo, Tango, Whiskey]
总结来说,当你有一个正在发出错误、错误项或没有任何项的Observable或Flowable操作时,从源头开始,逐步向下工作,直到找到导致问题的操作符。你还可以在每个步骤放置TestObserver以获取该操作中发生的更全面的报告,但使用doOnNext()、doOnError()、doOnComplete()、doOnSubscribe()等操作符是快速且简单的方法,可以深入了解管道该部分正在发生的事情。
可能不是最优的做法是必须使用doXXX()操作符修改代码来调试它。如果你使用 Intellij IDEA,你可以尝试在 lambda 中设置断点,尽管我使用这种方法只有混合的成功率。你还可以研究 RxJava 调试库,以获取不修改代码的详细日志。希望随着 RxJava 继续获得关注,将出现更多有用的调试工具,并成为标准。
摘要
在本章中,你学习了如何测试和调试 RxJava 代码。当你创建一个基于 RxJava 的应用程序或 API 时,你可能想要围绕它构建单元测试,以确保始终执行健全性检查。你可以使用阻塞操作符来帮助执行断言,但TestObserver和TestSubscriber将为你提供更全面和流畅的测试体验。你还可以使用TestScheduler来模拟时间流逝,以便可以立即测试基于时间的 Observables。最后,我们介绍了 RxJava 的调试策略,这通常涉及找到损坏的操作符,从源头开始,向下移动直到找到。
本章结束了我们对 RxJava 库的探索之旅,所以如果你已经到达这里,恭喜你!你现在有了构建响应式 Java 应用程序的坚实基础。在最后两章中,我们将涵盖 RxJava 在两个特定领域中的应用:Android 和 Kotlin。
第十一章:Android 上的 RxJava
如果有一个领域被反应式编程所席卷,那无疑是移动应用。正如本书中讨论的那样,ReactiveX 在许多领域都非常有用。但是,移动应用正变得越来越复杂,用户对无响应、缓慢或存在错误的应用的容忍度很低。因此,移动应用迅速成为 ReactiveX 的早期采用者,以解决这些问题。RxJava 在 Android 上站稳脚跟后,RxSwift 迅速在 iOS 上流行起来。还有 RxAndroid 和 RxBinding 库,可以将 RxJava 与 Android 环境轻松集成,我们将在本章中介绍。
Android 开发者长期以来一直面临的一个痛点是困于 Java 6。这意味着许多广泛使用的 Android 版本(如 KitKat、Lollipop 和 Marshmallow)不支持 Java 8 lambdas(尽管在 Android Nougat 中有所改变,它最终使用了 OpenJDK 8)。乍一看,这意味着你只能使用满是样板代码的匿名类来表达你的 RxJava 操作符(有关示例,请参阅附录 A)。然而,通过使用 Retrolambda,你实际上可以在使用 lambda 的同时使用更早版本的 Android,我们将在本章中介绍这一点。你还有另一个选择,那就是使用 Kotlin 语言,它已经成为 Android 开发的越来越受欢迎的平台。Kotlin 是一种可能比 Java 更现代、更具有表现力的语言,并且可以编译成 Java 6 字节码。我们将在下一章中介绍 Kotlin 与 RxJava 的结合使用。
如果你没有兴趣进行 Android 开发,请随意跳过本章。但本书的其余部分读者很可能是 Android 开发者,因此假设你已经进行了一些 Android 开发。
如果你几乎没有 Android 经验,但想学习,一本很好的入门书籍是 Bill Phillips、Chris Stewart 和 Kristin Marsicano 所著的 Android 编程:大牛牧场指南 (www.bignerdranch.com/books/android-programming/)。这是一本很好的书籍,可以帮助你快速精通 Android 开发。
本章将涵盖以下主题:
-
创建 Android 项目
-
为 Android 配置 RxJava
-
使用 RxJava 和 RxAndroid
-
使用 RxBinding
-
其他 Android Rx 库
创建 Android 项目
我们将在本章的示例中使用 Android Studio,以 Android 5.1 Lollipop 作为我们的平台目标。启动 Android Studio 并创建一个新项目,如图所示:
图 11.1:创建新的 Android 项目
在下一屏幕(如图所示),将你的项目命名为 RxJavaApp,公司域为 packtpub.com 或你喜欢的任何名称。然后,点击“下一步”:
图 11.2
我们将针对手机和平板。由于我们可能希望我们的应用与运行早期版本 Android 的设备兼容,让我们选择 Android 5.1(Lollipop)作为我们的最小 SDK。这将也给我们一个练习使用 Retrolambda 的机会。之后,点击下一步:
图 11.3
在下一个屏幕上,选择 Empty Activity 作为你的模板,如图所示。然后,点击下一步。正如你可能知道的,一个 activity 是一个包含控件的交互式屏幕。在本章的示例中,我们将使用一个 activity:
图 11.4
最后,我们来到配置 Activity 的最后一步。请随意将 Activity 名称留为MainActivity,其对应的布局名称为activity_main。我们稍后会填充这个 Activity。然后,点击完成:
图 11.5
你现在应该很快就会看到一个包含整个 Android 项目的屏幕,并且它应该已经配置了 Gradle。打开build.gradle (Module: app),这样我们就可以配置所需的依赖项,如下面的图所示:
图 11.6
你需要对针对 app 模块的build.gradle脚本进行一些修改,这样我们就可以使用 RxJava 和 Retrolambda。
配置 Retrolambda
首先,让我们设置 Retrolambda。我们还将利用一个快速单元测试来查看它是否正确工作。打开由项目模板创建的ExampleUnitTest.java文件。删除其中的示例单元测试方法,并声明一个新的方法名为lambdaTest()。在其内部,尝试使用 lambda 声明一个Callable<Integer>,如图所示。注意,它抛出了一个编译器错误,因为我们没有使用 Java 8 来支持 lambda。
图 11.7 - 此 Android 和 Java 版本不支持 lambda
如果我们针对的是 Android Lollipop,就不能使用 Java 8,因此我们需要 Retrolambda 来帮助我们避免创建满是样板代码的匿名内部类。它将在字节码级别将我们的 lambda 编译成匿名类,因此它支持 Java 6。
为了设置 Retrolambda,我们将使用 gradle-retrolambda 插件,使配置过程尽可能无缝。回到你的build.gradle (Module: app)脚本,并按照以下方式修改它:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'me.tatarka:gradle-retrolambda:3.6.1'
}
}
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.packtpub.rxjavademo"
minSdkVersion 22
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
保存脚本后,点击“立即同步”提示以重新构建项目。在前面代码中的重大变化是,我们添加了一个buildscript 块,它从mavenCentral()引入了 Retrolambda 3.6.1 作为依赖项。然后,我们可以应用 retrolambda 插件。最后,我们在 android 块内部添加一个compileOptions { }块,并将源和目标设置为与 Java 8 兼容。
现在运行包含我们 lambda 的单元测试。得分!如图所示,一切编译和运行都成功,我们现在可以在 Java 6 上运行 lambda 了!让我们看看:

图 11.8 - 现在我们可以使用 Retrolambda 在 Android Lollipop 上使用 Java 6 的 lambda
Retrolambda 是一个针对使用 Java 6 受限的 Android 开发者而言的绝佳工具。它巧妙地将 lambda 编译为传统的匿名类,使用 RxJava 时,你可以节省一些糟糕的样板工作。
要了解更多关于 Retrolambda 以及你可以进行的额外调整和配置,请查看其 GitHub 页面github.com/evant/gradle-retrolambda。在撰写本文时,Android Studio 中也有即将推出的 lambda 工具(developer.android.com/studio/preview/features/java8-support.html)。这些功能可能作为 Retrolambda 的替代品。
配置 RxJava 和相关库
现在艰难的部分已经结束,你已经设置了 Retrolambda,剩下的配置就是引入 RxJava 和 RxAndroid。需要添加到你的堆栈中的另一组库是 Jake Wharton 的 RxBinding (github.com/JakeWharton/RxBinding),它简化了 RxJava 在 Android UI 控件中的使用。
将这三个库添加到你的模块(不是 buildscript 块内的模块)的依赖项 { } 块中:
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
因此,现在应该是你的完整 build.gradle (Module: app) 内容:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'me.tatarka:gradle-retrolambda:3.6.1'
}
}
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.packtpub.rxjavademo"
minSdkVersion 22
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
testCompile 'junit:junit:4.12'
}
确保你点击“立即同步”提示以使用这些依赖项重新构建项目。在本章的剩余部分,我们将讨论你可以在 Android 应用程序中使用 RxJava、RxAndroid 和 RxBinding 的几种方式。我可以轻松地写一本关于你可以与 Android 一起使用的不同响应式功能、绑定和模式的小书,但在这章中,我们将采用极简主义方法,专注于核心 Rx 功能。我们将在本章末尾讨论你可以研究的其他库和资源。
使用 RxJava 和 RxAndroid
RxAndroid 库(github.com/ReactiveX/RxAndroid)的主要功能是它具有 Android Schedulers,可以帮助你实现 Android 应用程序的并发目标。它有一个针对 Android 主线程的 Scheduler,以及一个可以针对任何消息 Looper 的实现。RxAndroid 力求成为一个核心库,它没有许多其他功能。你需要专门的响应式绑定库来做更多的事情,我们将在稍后探讨。
让我们从简单开始。我们将修改MainActivity中间的TextView(它已经包含"Hello World!")在 3 秒后改为"Goodbye World!"。我们将使用Observable.delay()以响应式的方式完成所有这些。因为这将在一个计算调度器上发射,所以我们需要利用observeOn()来安全地将发射切换到 Android 主线程。
首先,在res/layout/activity_main.xml文件中,修改TextView块以具有一个 ID 属性名为my_text_view(如下所示)。这样,我们就可以在稍后从我们的应用程序代码中引用它:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.packtpub.rxjavademo.MainActivity">
<TextView
android:id="@+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
最后,重新构建你的项目并转到MainActivity.java文件。在onCreate()方法实现中,我们将查找我们的"my_text_view"组件并将其保存到一个名为myTextView的变量中(并将其转换为TextView)。
然后,立即,我们将创建一个只发出字符串Goodbye World!的Observable,并延迟 3 秒钟。因为delay()会将其放在计算调度器上,所以我们将使用observeOn()在接收到它后将那次发射放回AndroidSchedulers.mainThread()。按照以下代码所示实现所有这些:
package com.packtpub.rxjavademo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView myTextView = (TextView) findViewById(R.id.my_text_view);
Observable.just("Goodbye World!")
.delay(3, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> myTextView.setText(s));
}
}
在模拟的虚拟设备或实际连接的设备上运行此应用程序。确实,你将得到一个在 3 秒钟内显示"Hello World!"然后变为"Goodbye World!"的应用程序。在这里,我在一个虚拟 Pixel 手机上运行了这个应用程序,如图所示:

图 11.9 - 一个在 3 秒后将文本从"Hello World!"切换到"Goodbye World!"的 Android 应用程序。
如果你不用这个observeOn()操作切换回 Android 的mainThread(),应用程序很可能会崩溃。因此,确保任何修改 Android UI 的发射都在mainThread()上执行是很重要的。幸运的是,与传统的并发工具相比,RxJava 使这变得容易实现。
几乎你在本书中早期学到的所有内容都可以应用于 Android 开发,并且你可以将 RxJava 和 RxAndroid 与你的最爱 Android 实用工具、库和设计模式混合使用。然而,如果你想从 Android 小部件创建 Observables,你将需要使用 RxBinding 和其他库来增强你在 Android 上的 Rx 能力。
此外,还有一个AndroidSchedulers.from()工厂,它接受一个事件循环器并返回一个调度器,该调度器将在任何 Android 循环器上执行发射。这将在这个运行后台操作线程的线程上操作Observable/Flowable并通过onNext()发射结果。
使用 RxBinding
RxAndroid 没有创建从 Android 事件 Observables 的工具,但有许多库提供了这样做的方法。最受欢迎的库是 RxBinding,它允许你从 UI 小部件和事件创建 Observables。
RxBinding 中有大量的工厂可用。你可能经常使用的一个静态工厂类是 RxView,它允许你从扩展 View 的控件创建 Observables 并广播不同的事件作为发射。例如,将你的 activity_main.xml 修改为包含一个按钮和 TextView 类,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.packtpub.rxjavademo.MainActivity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_editor_absoluteY="8dp"
tools:layout_editor_absoluteX="8dp">
<Button
android:id="@+id/increment_button"
android:text="Increment"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
我们将 Button 和 TextView 分别保存为 increment_button 和 my_text_view ID。现在让我们切换到 MainActivity.java 类,让 Button 向 TextView 广播它被按下的次数。使用 RxView.clicks() 工厂将每个 Button 点击作为对象发出,并将其映射为 1。正如我们在 第三章 中所做的那样,基本操作,我们可以使用 scan() 操作符发出一个滚动计数,如下面的代码所示:
package com.packtpub.rxjavademo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.TextView;
import com.jakewharton.rxbinding2.view.RxView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView myTextView = (TextView) findViewById(R.id.my_text_view);
Button incrementButton = (Button) findViewById(R.id.increment_button);
//broadcast clicks into a cumulative increment, and display in TextView
RxView.clicks(incrementButton)
.map(o -> 1)
.scan(0,(total, next) -> total + next)
.subscribe(i -> myTextView.setText(i.toString()));
}
}
现在运行这个应用并按几次按钮。每次按下都会导致 TextView 中的数字增加,如下面的图所示:
)

图 11.10 - 响应式地将按钮点击转换为 scan() 发出被按下的次数。
仅在 RxView 中,就有数十个工厂可以发出 View 小部件上各种属性的 states 和 events。仅举几个例子,这些其他工厂包括 hover()、drag() 和 visibility()。还有针对不同小部件的大量专用工厂,例如 RxTextView、RxSearchView 和 RxToolbar。
RxBinding 中有如此多的功能,以至于很难在本章中涵盖所有内容。最有效的方法是探索 GitHub 上的 RxBinding 项目源代码,你可以在 github.com/JakeWharton/RxBinding/ 找到它。
注意,RxBinding 有几个可选的 "支持" 模块,你可以引入,包括设计绑定、RecyclerView 绑定,甚至 Kotlin 扩展。你可以在 GitHub README 中了解更多关于这些模块的信息。
其他 RxAndroid 绑定库
如果你完全采用响应式方法来制作 Android 应用,那么在你的应用中可以利用许多其他专门的响应式绑定库。它们通常处理 Android 的特定领域,但如果你在这些领域工作,它们仍然可能是有帮助的。除了 RxBinding 之外,以下是一些你可以用响应式方式与 Android 一起使用的知名绑定库:
-
SqlBrite (
github.com/square/sqlbrite):一个 SQLite 包装器,为 SQL 查询带来响应式语义。 -
RxLocation (
github.com/patloew/RxLocation):一个响应式位置 API -
rx-preferences (
github.com/f2prateek/rx-preferences):一个响应式 SharedPreferences API -
RxFit (
github.com/patloew/RxFit):Android 的响应式健身 API -
RxWear (
github.com/patloew/RxWear):Wearable 库的响应式 API -
ReactiveNetwork (
github.com/pwittchen/ReactiveNetwork):响应式监听网络连接状态 -
ReactiveBeacons (
github.com/pwittchen/ReactiveBeacons):响应式扫描附近的 BLE(低功耗蓝牙)信标
正如你所见,Android 有一个相当完整的 RxJava 生态系统,你可以在 RxAndroid 维基页面上查看更完整的列表(github.com/ReactiveX/RxAndroid/wiki)。当然要利用 Google 来查看是否有适合你特定任务的库。如果你找不到库,可能有一个开源的机会来创建一个!
使用 RxJava 与 Android 的生命周期和注意事项
始终要谨慎地管理你的订阅生命周期。确保你的 Android 应用中不要依赖弱引用,并且不要假设响应式流会自动释放资源,因为它们不会!所以,当你的 Android 应用的一部分不再被使用时,始终要调用 dispose() 方法来释放你的 disposables。
例如,假设你创建了一个简单的应用,显示自启动以来的秒数。为了这个练习,设置你的布局如下,以便在 TextView 类中有 timer_field:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.packtpub.rxjavaapp.MainActivity">
<TextView
android:id="@+id/timer_field"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
我们可以使用 Observable.interval() 来每秒向 TextField 发射一次。但我们需要仔细决定当应用不再活跃时,这个计数器如何以及是否持续。当 onPause() 被调用时,我们可能想要释放这个计时器操作。当 onResume() 被调用时,我们可以再次订阅并创建一个新的可释放对象,从而重新启动计时器。为了保险起见,当 onDestroy() 被调用时也应该释放它。以下是一个简单的实现,它管理了这些生命周期规则:
package com.packtpub.rxjavaapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
public class MainActivity extends AppCompatActivity {
private final Observable<String> timer;
private Disposable disposable;
MainActivity() {
timer = Observable.interval(1, TimeUnit.SECONDS)
.map(i -> Long.toString(i))
.observeOn(AndroidSchedulers.mainThread());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onPause() {
super.onPause();
disposable.dispose();
}
@Override
protected void onResume() {
super.onResume();
TextView tv = (TextView) findViewById(R.id.timer_field);
disposable = timer.subscribe(s -> tv.setText(s));
}
@Override
protected void onDestroy() {
super.onDestroy();
if (disposable != null)
disposable.dispose();
}
}
如果你想要持久化或保存你应用的状态,你可能需要发挥创意,找到一种方法在 onPause() 被调用时释放你的响应式操作,同时允许它在 onResume() 发生时从上次离开的地方继续。在下面的代码中,我持有一个来自计时器的最后一个值在 inAtomicInteger 中,并在发生暂停/恢复事件时使用它作为新的订阅的起始值:
package com.packtpub.rxjavaapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
public class MainActivity extends AppCompatActivity {
private final Observable<String> timer;
private final AtomicInteger lastValue = new AtomicInteger(0);
private Disposable disposable;
MainActivity() {
timer = Observable.interval(1, TimeUnit.SECONDS)
.map(i -> 1)
.startWith(Observable.fromCallable(lastValue::get))
.scan((current,next) -> current + next)
.doOnNext(lastValue::set)
.map(i -> Integer.toString(i))
.observeOn(AndroidSchedulers.mainThread());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onPause() {
super.onPause();
disposable.dispose();
}
@Override
protected void onResume() {
super.onResume();
TextView tv = (TextView) findViewById(R.id.timer_field);
disposable = timer.subscribe(s -> tv.setText(s));
}
@Override
protected void onDestroy() {
super.onDestroy();
if (disposable != null)
disposable.dispose();
}
}
因此,再次确保你仔细管理你的响应式操作,并随着你应用的生命周期有意识地释放它们。
此外,确保当有多个观察者/订阅者监听 UI 事件时,利用多播。这可以防止多个监听器附加到小部件上,这可能并不总是高效的。另一方面,当只有一个 Observer/Subscriber 监听小部件的事件时,不要添加多播的开销。
摘要
在本章中,我们简要介绍了丰富的 RxAndroid 生态系统中的各个部分,以构建响应式 Android 应用程序。我们介绍了 Retrolambda,这样我们就可以利用 lambda 表达式来支持仅支持 Java 6 的早期 Android 版本。这样,我们就不必求助于匿名内部类来表达我们的 RxJava 操作符。我们还简要介绍了 RxAndroid,它是响应式 Android 生态系统的核心,它只包含 Android 调度器。为了将各种 Android 小部件、控件和特定领域的事件连接起来,你将需要依赖其他库,例如 RxBinding。
在下一章中,我们将介绍如何使用 Kotlin 与 RxJava 结合。我们将学习如何使用这一激动人心的新语言,它本质上已经成为了 Android 的 Swift,以及它为何与 RxJava 配合得如此之好。
第十二章:使用 RxJava 为 Kotlin 新增功能
在我们的最后一章中,我们将把 RxJava 应用于 JVM 上的一个令人兴奋的新领域:Kotlin 语言。
Kotlin 由 JetBrains 开发,该公司还开发了 IntelliJ IDEA、PyCharm 以及其他几个主要的 IDE 和开发者工具。JetBrains 之前一直使用 Java 来构建其产品,但自 2010 年以来,JetBrains 开始质疑是否有一种更好的语言来满足他们的需求和现代需求。在调查了现有的语言后,他们决定构建并开源自己的语言。2016 年(5 年后),Kotlin 1.0 发布。2017 年,Kotlin 1.1 发布,面向日益增长的用户群体。不久之后,Google 宣布 Kotlin 为 Android 的官方支持语言。
在本章中,我们将涵盖以下主题:
-
为什么选择 Kotlin?
-
配置 Kotlin
-
Kotlin 基础
-
扩展操作符
-
使用 RxKotlin
-
处理 SAM 模糊性
-
let()和apply() -
元组和数据类
-
ReactiveX 和 Kotlin 的未来
为什么选择 Kotlin?
Kotlin 力求成为一个实用和行业导向的语言,寻求最小(但可读性)的语法,以表达业务逻辑而不是样板代码。然而,它不像许多简洁的语言那样走捷径。它是静态类型的,在生产环境中表现稳健,同时足够快速,可以用于原型设计。它还可以与 Java 库和源代码 100% 兼容,使得逐步过渡成为可能。
直到最近还困于 Java 6 的 Android 开发者迅速采用了 Kotlin,并有效地将其变成了“Android 的 Swift”。有趣的是,Swift 和 Kotlin 有类似的感受和语法,但 Kotlin 出现得要早。除此之外,Kotlin 社区和库生态系统也在迅速增长。2017 年,Google 宣布 Kotlin 为官方支持的开发 Android 应用程序的语言。由于 JetBrains 和 Google 的承诺,Kotlin 在 JVM 上的光明未来是显而易见的。
但 Kotlin 与 RxJava 有何关系?Kotlin 有许多 Java 所不具备的有用语言特性,它们可以极大地提高 RxJava 的可表达性。此外,越来越多的 Android 开发者正在使用 Kotlin 和 RxJava,因此展示这两个平台如何协同工作是有意义的。
Kotlin 是一种 Java 开发者可以在几天内快速掌握的语言。如果你想要详细了解 Kotlin,Dmitry Jemerov 和 Svetlana Isakova 所著的《Kotlin in Action》(www.manning.com/books/kotlin-in-action) 是一本极佳的书籍。此外,JetBrains 还提供了优秀的在线参考文档(kotlinlang.org/docs/reference/)。在本章中,我们将快速浏览 Kotlin 的几个基本特性,以展示其在表达 RxJava 时的相关性。
配置 Kotlin
您可以使用 Gradle 或 Maven 来构建您的 Kotlin 项目。您可以在 Intellij IDEA 中创建一个新的 Kotlin 项目而不需要任何构建自动化,但以下是设置 Kotlin 项目用于 Gradle 和 Maven 的方法。
配置 Kotlin 用于 Gradle
要使用 Kotlin 语言与 Gradle,首先将以下构建脚本 { } 块添加到您的 build.gradle 文件中:
buildscript {
ext.kotlin_version = '<version to use>'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-
plugin:$kotlin_version"
}
}
然后,您需要应用以下代码中的插件,以及将包含源代码的目录。
注意,src/main/kotlin 已经默认指定,但如果需要,您可以使用 sourceSets { } 块指定不同的目录:
apply plugin: "kotlin"
sourceSets {
main.kotlin.srcDirs += 'src/main/kotlin'
}
您可以在 Kotlin 网站上详细了解 Kotlin Gradle 配置,请访问 kotlinlang.org/docs/reference/using-gradle.html。
配置 Kotlin 用于 Maven
对于 Maven,在 POM 文件中定义 kotlin.version 属性并将 Kotlin-stdlib 作为依赖项,如下面的代码所示。然后,构建项目:
<properties>
<kotlin.version>1.1.2-2</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
您还需要指定源代码目录和 kotlin-maven-plugin,如下面的代码所示:
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals> <goal>compile</goal> </goals>
</execution>
<execution>
<id>test-compile</id>
<goals> <goal>test-compile</goal> </goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
您可以在 Kotlin 网站上详细了解 Kotlin Maven 配置,请访问 kotlinlang.org/docs/reference/using-maven.html。
配置 RxJava 和 RxKotlin
在本章中,我们还将使用 RxJava 以及一个名为 RxKotlin 的扩展库。对于 Gradle,将这两个库作为依赖项添加,如下所示:
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'io.reactivex.rxjava2:rxkotlin:2.0.2'
对于 Maven,设置方式如下:
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxkotlin</artifactId>
<version>2.0.2</version>
</dependency>
Kotlin 基础
虽然 Kotlin 有一个独立的编译器并且可以与 Eclipse 一起工作,但我们将使用 Intellij IDEA。
Kotlin 项目的结构类似于 Java 项目。遵循标准的 Maven 习惯,您通常将 Kotlin 源代码放在 /src/main/kotlin/ 文件夹中,而不是 /src/main/java/ 文件夹中。Kotlin 源代码存储在具有 .kt 扩展名的文本文件中,而不是 .java 扩展名。然而,Kotlin 文件不必包含与文件同名的类。
创建 Kotlin 文件
在 Intellij IDEA 中,如果您还没有导入 Kotlin 项目,请右键单击 /src/main/kotlin/ 文件夹并导航到 New | Kotlin File/Class,如图所示:
图 12.1: 创建一个新的 Kotlin 文件
在以下对话框中,将文件命名为 Launcher,然后点击 OK。现在您应该在项目视图中看到 Launcher.kt 文件。双击它以打开编辑器。编写以下 "Hello World" Kotlin 代码,如图所示,然后通过点击侧边栏中的 K 图标来运行它:

这是我们第一个 Kotlin 应用程序。Kotlin 使用“函数”而不是方法,但它有一个 main() 函数,就像 Java 有一个 main() 方法一样。请注意,我们不需要在 Java 类中放置我们的 main() 函数。这是 Kotlin 的一个好处。尽管它编译成 Java 字节码,但你不仅限于面向对象的约定,也可以是过程式或函数式的。
分配属性和变量
要声明一个变量或属性,你必须决定是否使其可变。在变量声明前加上 val 将使其只能赋值一次,而 var 是可变的,可以多次重新赋值。变量的名称随后跟一个冒号,将其与类型分开。然后,如果你有值,你可以分配它。在下面的代码中,我们为 Int 和 String 分配变量,并在插值字符串中打印它们:
fun main(args: Array<String>) {
val myInt: Int = 5
val myString: String = "Alpha"
println("myInt=$myInt and myString=$myString")
}
输出如下:
myInt=5 and myString=Alpha
Kotlin 的编译器非常智能,并不总是需要显式声明变量和属性的类型。如果你立即给它赋值,它将从这个值中推断类型。因此,我们可以按照以下方式移除类型声明:
fun main(args: Array<String>) {
val myInt = 5 *//infers type as `Int`*
val myString = "Alpha" *//infers type as `String`*
println("myInt=$myInt and myString=$myString")
}
扩展函数
当你在 Kotlin 中进行 RxJava 工作时,创建扩展函数是非常有帮助的。我们将在稍后具体介绍,但这里有一个非响应式示例。
假设我们想要向 LocalDate 添加一个方便的函数,以便快速计算到另一个 LocalDate 的天数。而不是反复调用冗长的辅助类来完成这个任务,我们可以快速向 LocalDate 添加一个名为 numberOfDaysTo() 的扩展函数,如下所示。这并不扩展 LocalDate,而是让编译器将其解析为一个静态方法:
import java.time.LocalDate
import java.time.temporal.ChronoUnit
fun main(args: Array<String>) {
val startDate = LocalDate.of(2017,5,1)
val endDate = LocalDate.of(2017,5,11)
val daysBetween = startDate.numberOfDaysTo(endDate)
println(daysBetween)
}
fun LocalDate.numberOfDaysTo(otherLocalDate: LocalDate): Long {
return ChronoUnit.DAYS.between(this, otherLocalDate)
}
输出如下:
10
扩展函数在 Kotlin 中就像一个普通函数,但你必须立即声明你要添加函数的类型,然后是一个点,接着是扩展函数的名称(例如,fun LocalDate.numberOfDaysTo())。在随后的代码块中,它将目标 LocalDate 视为 this,就像它是在类内部一样。但再次强调,所有这些都会在编译时解析为一个静态方法。Kotlin 会神奇地将这些抽象化处理。
这允许你创建一个更流畅的 DSL(领域特定语言),它针对你的特定业务进行了优化。作为额外的奖励,IntelliJ IDEA 将在处理 LocalDate 时自动完成此扩展函数。
由于这个扩展函数的主体只有一行,实际上你可以使用等于(=)语法来更简洁地声明一个函数,并省略返回关键字以及显式的类型声明,如下面的代码所示:
fun LocalDate.numberOfDaysTo(otherLocalDate: LocalDate) =
ChronoUnit.DAYS.between(this, otherLocalDate)
正如我们将很快看到的,Kotlin 扩展函数是向 Observables 和 Flowables 添加新操作符的强大工具,并且它们比 compose() 和 lift() 提供了更多的灵活性和便利性。但首先,让我们看看 Kotlin 的 lambda。
Kotlin lambda
我本可以花很多时间来解析 Kotlin 中的 lambda,但为了“直奔主题”,我将展示它们在 RxJava 中的表达方式。你可以在 Kotlin 参考网站上深入了解 Kotlin lambda(kotlinlang.org/docs/reference/lambdas.html)。
Kotlin 提供了比 Java 8 更多的表达 lambda 的方式,并且它还使用花括号 { } 而不是圆括号 ( ) 来将 lambda 参数接受到函数中。以下是如何表达一个发射字符串并映射并打印其长度的 Observable 链:
import io.reactivex.Observable
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gama", "Delta", "Epsilon")
.map { s: String -> s.length }
.subscribe { i: Int -> println(i) }
}
输出如下:
5
4
4
5
7
注意我们如何表达 map() 和 subscribe() 的 lambda 参数。一开始使用花括号 { } 接受 lambda 参数可能会觉得有些奇怪,但不久之后就会觉得非常自然。它们有助于区分有状态参数和无状态参数。如果你喜欢,可以围绕它们放置圆括号,但这很混乱,并且只有在需要传递多个 lambda 参数(如 collect() 操作符)时才需要:
import io.reactivex.Observable
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gama", "Delta", "Epsilon")
.map( { s: String -> s.length } )
.subscribe( { i: Int -> println(i) } )
}
如前所述,Kotlin 编译器在类型推断方面非常智能。因此,大多数情况下,我们不需要将我们的 lambda 参数 s 或 i 声明为 String 和 Int。编译器可以为我们解决这个问题,如下面的代码所示:
import io.reactivex.Observable
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gama", "Delta", "Epsilon")
.map { s -> s.length }
.subscribe { i -> println(i) }
}
更好的是,这些是简单的单参数 lambda,所以我们甚至不需要命名这些参数。我们可以完全省略它们,并使用 it 关键字来引用,如下所示:
import io.reactivex.Observable
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gama", "Delta", "Epsilon")
.map { it.length }
.subscribe { println(it) }
}
与 Java 8 类似,我们也可以使用函数引用语法。如果我们只是以完全相同的方式和顺序将参数传递给函数或构造函数,我们可以使用双冒号 :: 语法,如下所示。请注意,我们在这里确实使用了圆括号:
import io.reactivex.Observable
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gama", "Delta", "Epsilon")
.map(String::length)
.subscribe(::println)
}
关于 Kotlin lambda 参数的另一个有趣之处在于,当你有多个参数且最后一个参数是 lambda 时,你可以将 lambda 表达式放在圆括号之外。在下面的代码中,scan() 发射字符串长度的滚动总和,并提供一个种子值 0。然而,我们可以将最后的 lambda 参数放在圆括号 ( ) 之外:
import io.reactivex.Observable
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gama", "Delta", "Epsilon")
.map { s: String -> s.length }
.scan(0) { total, next -> total + next }
.subscribe {
println("Rolling sum of String lengths is $it")
}
}
扩展操作符
如前所述,Kotlin 提供了扩展函数。这些可以作为仅使用 compose() 和 lift() 的强大替代方案。
例如,我们无法使用 Transformers 和 compose() 将 Observable<T> 转换为 Single<R>。但使用 Kotlin 扩展函数,这完全可行。在下面的代码中,我们创建了一个 toSet() 操作符并将其添加到 Observable<T>:
import io.reactivex.Observable
fun main(args: Array<String>) {
val source = Observable.just("Alpha", "Beta", "Gama", "Delta",
"Epsilon")
val asSet = source.toSet()
}
fun <T> Observable<T>.toSet() =
collect({ HashSet<T>() }, { set, next -> set.add(next) })
.map { it as Set<T> }
toSet()返回一个Single<Set<T>>,并且它被调用在一个Observable<T>上。在扩展函数中,collect()运算符被调用在调用的Observable上,然后它将HashSet转换为Set,因此实现是隐藏的。正如你所见,创建新的运算符并使其易于发现是很简单的。
你也可以使扩展函数仅针对某些泛型类型。例如,我可以创建一个仅针对Observable<Int>的sum()扩展函数(Int是 Kotlin 中的 Integer/int 抽象类型)。它只有在与发出整数的Observable一起使用时才有效,并且只能编译或出现在该类型的自动完成中:
import io.reactivex.Observable
fun main(args: Array<String>) {
val source = Observable.just(100, 50, 250, 150)
val total = source.sum()
}
fun Observable<Int>.sum() =
reduce(0) { total, next -> total + next }
使用 RxKotlin
有一个名为 RxKotlin 的小型库(github.com/ReactiveX/RxKotlin/),我们在本章开头将其作为依赖项。在撰写本文时,它几乎不是一个复杂的库,而是一组用于常见反应式转换的方便扩展函数的集合。它还试图在 Kotlin 中使用 RxJava 时标准化一些约定。
例如,有toObservable()和toFlowable()扩展函数可以在可迭代的、序列的以及一些其他来源上调用。在下面的代码中,我们不是使用Observable.fromIterable()将List转换为Observable,而是直接调用它的toObservable()扩展函数:
import io.reactivex.rxkotlin.toObservable
fun main(args: Array<String>) {
val myList = listOf("Alpha", "Beta", "Gamma", "Delta",
"Epsilon")
myList.toObservable()
.map(String::length)
.subscribe(::println)
}
在 RxKotlin 中还有一些值得探索的其他扩展功能,你可以在 GitHub 页面上查看所有内容。这个库故意保持小巧和专注,因为它很容易让 API 因为每个可能的任务都添加一个扩展函数而变得杂乱。但它包含了常见任务的功能,如前面的例子。
RxKotlin 还有一些有用的帮助程序可以解决 Java 和 Kotlin 之间存在的 SAM 问题(如果你已经尝试过,你可能已经注意到了这个问题)。我们将在下一节中介绍这个问题。
处理 SAM 歧义
在撰写本文时,当 Kotlin 使用功能参数调用 Java 库时,存在一个细微差别。这个问题在 RxJava 2.0 中尤其明显,因为引入了许多参数重载。当调用 Kotlin 库时,Kotlin 没有这个问题,但与 Java 库一起使用时却存在。当给定 Java 方法上有多个针对不同功能 SAM 类型的参数重载时,Kotlin 会在推断中迷失方向,需要帮助。在 JetBrains 解决这个问题之前,你需要通过明确指定或使用 RxKotlin 的帮助程序来解决这个问题。
这里有一个臭名昭著的例子:zip()运算符。尝试在这里进行简单的zip操作,你会因为类型推断失败而得到编译错误:
import io.reactivex.Observable
fun main(args: Array<String>) {
val strings = Observable.just("Alpha", "Beta", "Gamma",
"Delta")
val numbers = Observable.range(1,4)
*//compile error, can't infer parameters*
val zipped = Observable.zip(strings, numbers) { s,n -> "$s $n" }
zipped.subscribe(::println)
}
解决这个问题的一种方法是用你的 lambda 显式地构造 SAM 类型。在这种情况下,我们需要告诉编译器我们正在给它一个BiFunction<String,Int,String>,如下所示:
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
fun main(args: Array<String>) {
val strings = Observable.just("Alpha", "Beta", "Gamma",
"Delta")
val numbers = Observable.range(1,4)
val zipped = Observable.zip(strings, numbers,
BiFunction<String,Int,String> { s,n -> "$s $n" }
)
zipped.subscribe(::println)
}
不幸的是,这相当冗长。许多人使用 RxJava 和 Kotlin 来减少代码量,而不是增加,所以这不是理想的情况。幸运的是,RxKotlin 提供了一些工具来解决这个问题。你可以使用 Observables、Flowables、Singles 和 Maybes 工具类来调用受 SAM 问题影响的工厂实现。以下是我们使用这种方法的一个例子:
import io.reactivex.Observable
import io.reactivex.rxkotlin.Observables
fun main(args: Array<String>) {
val strings = Observable.just("Alpha", "Beta", "Gamma",
"Delta")
val numbers = Observable.range(1,4)
val zipped = Observables.zip(strings, numbers) { s, n -> "$s $n" }
zipped.subscribe(::println)
}
对于受 SAM 问题影响的非工厂操作符,也存在扩展函数。以下是我们使用 zipWith() 扩展函数的一个例子,该函数成功地使用我们的 Kotlin lambda 参数进行了推理。请注意,我们必须导入这个扩展函数才能使用它:
import io.reactivex.Observable
import io.reactivex.rxkotlin.zipWith
fun main(args: Array<String>) {
val strings = Observable.just("Alpha", "Beta", "Gamma",
"Delta")
val numbers = Observable.range(1,4)
val zipped = strings.zipWith(numbers) { s, n -> "$s $n" }
zipped.subscribe(::println)
}
还应该指出,Single 和 Maybe 上的 subscribe() 也受到 SAM 不明确性问题的困扰,因此有 subscribeBy() 扩展来处理它,如下所示:
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
fun main(args: Array<String>) {
Observable.just("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
.count()
.subscribeBy { println("There are $it items") }
}
务必不要让 SAM 不明确性问题阻碍你尝试 Kotlin。这是 Kotlin lambda 与 Java SAM 类型互操作时的一个细微差别。这个问题已被 JetBrains 承认,应该是暂时的。此外,在 Kotlin 社区中也有讨论,为了其他原因创建一个纯 Kotlin 的 ReactiveX 实现,我们将在本章末尾讨论 RxKotlin 的未来。
使用 let() 和 apply()
在 Kotlin 中,每个类型都有一个 let() 和 apply() 扩展函数。这两个简单但实用的工具可以使你的代码更加流畅和易于表达。
使用 let()
let() 简单地接受一个将调用对象 T 映射到另一个对象 R 的 lambda 表达式。这与 RxJava 提供的 to() 操作符类似,但它适用于任何类型 T,而不仅仅是 Observables/Flowables。例如,我们可以对一个已经被转换为小写的字符串调用 let(),然后立即对其进行任何任意的转换,例如将其 reversed() 字符串连接到它。看看这个操作:
fun main(args: Array<String>) {
val str = "GAMMA"
val lowerCaseWithReversed = str.toLowerCase().let { it + " " +
it.reversed() }
println(lowerCaseWithReversed)
}
输出如下:
gamma ammag
当你不想为了多次引用而将值保存到变量中时,let() 就派上用场了。在上面的代码中,我们不必将 toLowerCase() 的结果保存到变量中。相反,我们只需立即对它调用 let() 来完成我们需要的操作。
在 RxJava 的上下文中,let() 函数可以帮助快速处理一个 Observable,将其分支,然后使用组合操作符重新组合。在下面的代码中,我们将数字的 Observable 分发给一个 let() 操作符,该操作符创建一个总和和一个计数,然后返回使用这两个值来找到平均值的 zipWith() 操作符的结果:
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.rxkotlin.zipWith
fun main(args: Array<String>) {
val numbers =
Observable.just(180.0, 160.0, 140.0, 100.0, 120.0)
val average = numbers.publish()
.autoConnect(2)
.let {
val sum = it.reduce(0.0) { total, next -> total +
next}
val count = it.count()
sum.zipWith(count) { s, c -> s / c }
}
average.subscribeBy(::println)
}
输出如下:
140.0
let() 中的最后一行是返回的内容,不需要返回关键字。
总结来说,let() 是一个强大且简单的工具,可以流畅地将一个项目转换为另一个项目。在 RxJava 中,使用它来分支 Observable 或 Flowable 流,然后再将它们重新组合,是它的一个有用应用。
使用 apply()
与 let() 类似的工具是 apply()。与 let() 将 T 类型的项转换为 R 类型的项不同,apply() 对 T 类型的项执行一系列操作,然后再返回相同的 T 类型的项。这在声明一个项 T 但对其执行辅助操作而不打断声明/赋值流程时非常有用。
这里是一个非响应式示例。我们有一个简单的类 MyItem,它有一个 startProcess() 方法。我们可以实例化 MyItem,但使用 apply() 在将 MyItem 赋值给变量之前调用这个 startProcess() 方法,如下所示:
fun main(args: Array<String>) {
val myItem = MyItem().apply {
startProcess()
}
}
class MyItem {
fun startProcess() = println("Starting Process!")
}
输出如下:
Starting Process!
在 RxJava 中,apply() 函数有助于在 Observable/Flowable 链中添加观察者或订阅者,同时不会打断当前主要任务的流程。这有助于向单独的流中发送状态消息。
在以下代码中,我们发出五个 1 秒的间隔,并乘以每个间隔。然而,我们在乘法之前在 apply() 中创建了一个 statusObserver 并订阅它。在 apply() 之前也进行了多播,以便将发射物推送到两个目的地:
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import java.util.concurrent.TimeUnit
fun main(args: Array<String>) {
val statusObserver = PublishSubject.create<Long>()
statusObserver.subscribe { println("Status Observer: $it") }
Observable.interval(1, TimeUnit.SECONDS)
.take(5)
.publish()
.autoConnect(2)
.apply {
subscribe(statusObserver)
}
.map { it * 100 }
.subscribe {
println("Main Observer: $it")
}
Thread.sleep(7000)
}
输出如下:
Status Observer: 0
Main Observer: 0
Status Observer: 1
Main Observer: 100
Status Observer: 2
Main Observer: 200
Status Observer: 3
Main Observer: 300
Status Observer: 4
Main Observer: 400
因此,再次强调,apply() 在将多播流发射物推送到多个观察者时非常有用,而不需要任何中间变量。
与 apply() 类似的扩展函数是 run(),它执行一系列操作,但返回类型为空(或在 Kotlin 中称为 Unit)。还有 with(),它与 run() 相同,但它不是一个扩展函数。它接受目标项作为参数。
元组和数据类
Kotlin 对元组支持有限,但它还提供了更好的数据类。我们将在 RxJava 的上下文中探讨这两个工具。
Kotlin 支持快速创建包含两个项的 Pair(这些项可以是不同类型的)。这是一个简单的双值元组,但具有静态类型。你可以通过在两个值之间放置 to 关键字来快速构建一个。这在需要在两个流之间执行 zip() 操作并将两个项配对时非常有用。
在以下代码中,我们将字符串项的流与 Int 项的流进行连接,并将每个配对放入 Pair<String,Int>。
import io.reactivex.Observable
import io.reactivex.rxkotlin.Observables
fun main(args: Array<String>) {
val strings = Observable.just("Alpha", "Beta", "Gamma",
"Delta")
val numbers = Observable.range(1,4)
*//Emits Pair<String,Int>*
Observables.zip(strings, numbers) { s, n -> s to n }
.subscribe {
println(it)
}
}
输出如下:
(Alpha, 1)
(Beta, 2)
(Gamma, 3)
(Delta, 4)
更好的方法是使用数据类。数据类是 Kotlin 的一个强大工具,它的工作方式与类相似,但会自动实现 hashcode()/equals()、toString(),以及一个巧妙的 copy() 函数,允许你克隆并修改属性到该类的新实例。
但目前,我们将仅使用数据类作为比 Pair 更干净的方法,因为我们实际上为每个属性提供了一个名称,而不是 first 和 second。在以下代码中,我们将创建一个 StringAndNumber 数据类,并使用它来连接每个值对:
import io.reactivex.Observable
import io.reactivex.rxkotlin.Observables
fun main(args: Array<String>) {
val strings = Observable.just("Alpha", "Beta", "Gamma",
"Delta")
val numbers = Observable.range(1,4)
data class StringAndNumber(val myString: String, val myNumber: Int)
Observables.zip(strings, numbers) { s, n -> StringAndNumber(s,n) }
.subscribe {
println(it)
}
}
输出如下:
StringAndNumber(myString=Alpha, myNumber=1)
StringAndNumber(myString=Beta, myNumber=2)
StringAndNumber(myString=Gamma, myNumber=3)
StringAndNumber(myString=Delta, myNumber=4)
数据类(以及普通的 Kotlin 类)声明快捷且简单,因此你可以战略性地使用它们来完成小型任务。使用它们可以使你的代码更清晰且易于维护。
ReactiveX 和 Kotlin 的未来
Kotlin 是一种强大且实用的语言。JetBrains 不仅投入了大量努力使其有效,还使其与现有的 Java 代码和库兼容。尽管存在一些粗糙的地方,比如 SAM lambda 推断,但他们仍然出色地让 Java 和 Kotlin 一起工作。然而,即使有了这种坚实的兼容性,许多开发者也渴望完全迁移到 Kotlin 以利用其功能。命名参数、可选参数、可空类型、扩展函数、内联函数、委托和其他语言特性使 Kotlin 对专用使用具有吸引力。更不用说,JetBrains 已经成功地将 Kotlin 编译成 JavaScript,并将很快支持 LLVM 原生编译。纯 Kotlin 编写的库可以潜在地编译到所有这些平台。为了进一步巩固 Kotlin 的地位,谷歌官方将其确立为 Android 的下一个支持语言。
因此,这引发了一个问题:在纯 Kotlin 中创建 ReactiveX 实现并不仅仅依赖于 RxJava 是否会有好处?毕竟,Kotlin 语言有一套强大的功能,可以为 ReactiveX 实现提供很多帮助,并将其带到 Kotlin 可以编译到的多个平台。它还将创建一个针对 Kotlin 优化的 ReactiveX 体验,支持可空类型发射、扩展操作符和基于协程的并发。
协程为 Kotlin 应用程序快速(且更安全地)实现并发提供了一个有趣且有用的抽象。因为协程支持任务挂起,它们提供了一种自然机制来支持背压。如果在 Kotlin 中实现 ReactiveX,协程可以在使背压简单实现方面发挥巨大作用。
如果你想了解如何在 Kotlin 中利用 Kotlin 协程创建 ReactiveX 实现,请阅读 Roman Elizarov 的迷人文章,链接为 github.com/Kotlin/kotlinx.coroutines/blob/master/reactive/coroutines-guide-reactive.md。
因此,在纯 Kotlin 中实现 ReactiveX 实现确实可能带来很多好处。在撰写本文时,这个话题在 Kotlin 社区中越来越受欢迎。请密切关注,因为人们将继续实验,从概念验证逐步发展到原型,然后是官方发布。
摘要
在本章中,我们介绍了如何使用 RxJava 进行 Kotlin 编程。Kotlin 语言为在 JVM 上更实用地表达代码提供了激动人心的机会,而 RxJava 可以利用其许多有用特性。扩展函数、数据类、RxKotlin 以及如 let()/apply() 这样的函数式操作符使你更容易地表达你的响应式领域。尽管 SAM 推断可能会让你遇到障碍,但你可以通过利用 RxKotlin 的辅助工具来解决这个问题,直到 JetBrains 提供修复方案。将来,看到是否会出现纯 Kotlin 实现的 ReactiveX 将会很有趣。这样的实现将引入 Kotlin 允许而 Java 不允许的大量功能。
这就是终点!如果你从头到尾阅读了这本书,恭喜你!你应该已经具备了在工作和项目中利用 RxJava 的坚实基础。响应式编程是一种彻底不同的解决问题方法,但也是非常有效的。响应式编程将继续增长其相关性,并塑造我们建模代码的未来。站在这个前沿将使你不仅具有市场竞争力,而且在未来几年内成为领导者。
附录
本附录将带你了解 lambda 表达式、函数式类型、面向对象和响应式编程的混合以及调度器的工作原理。
引入 lambda 表达式
Java 在 2014 年发布的 Java 8 中正式支持 lambda 表达式。Lambda 表达式 是 单抽象方法 (SAM) 类的简写实现。换句话说,它们是传递函数式参数而不是匿名类的快捷方式。
将 Runnable 转换为 lambda
在 Java 8 之前,你可能已经利用匿名类即时实现接口,例如 Runnable,如下所示:
public class Launcher {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("run() was called!");
}
};
runnable.run();
}
}
输出如下:
run() was called!
要实现 Runnable 而不声明一个显式的类,你必须立即在构造函数之后在一个块中实现其 run() 抽象方法。这产生了大量的样板代码,并成为 Java 开发的一个主要痛点,也是使用 Java 进行函数式编程的障碍。幸运的是,Java 8 正式将 lambda 引入到 Java 语言中。有了 lambda 表达式,你可以用更简洁的方式表达这一点:
public class Launcher {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("run() was
called!");
runnable.run();
}
}
太棒了,对吧?这减少了大量的代码和样板,我们将深入探讨它是如何工作的。Lambda 表达式可以针对任何只有一个抽象方法的接口或抽象类,这些类型被称为 单抽象方法 类型。在上面的代码中,Runnable 接口有一个名为 run() 的单抽象方法。如果你传递一个与该抽象方法的参数和返回类型匹配的 lambda,编译器将使用该 lambda 来实现该方法。
-> 箭头左侧是参数。Runnable 的 run() 方法不接收任何参数,因此 lambda 使用空括号 () 提供没有参数。箭头 -> 的右侧是要执行的操作。在这个例子中,我们调用一个单条语句并使用 System.out.println("run() was called!"); 打印一个简单的消息。
Java 8 的 lambda 表达式可以在主体中支持多个语句。比如说,我们有一个包含多个语句的 Runnable 匿名内部类,其 run() 方法的实现如以下代码片段所示:
public class Launcher {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Message 1");
System.out.println("Message 2");
}
};
runnable.run();
}
}
你可以将两个 System.out.println() 语句移动到 lambda 中,通过在箭头 -> 右侧的 { } 多行块中包裹它们。请注意,你需要在 lambda 中的每一行使用分号来终止,如下所示:
public class Launcher {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println("Message 1");
System.out.println("Message 2");
};
runnable.run();
}
}
将 Supplier 转换为 lambda
Lambda 表达式还可以实现返回项的方法。例如,Java 8 中引入的 Supplier 类(最初由 Google Guava 引入)有一个抽象的 get() 方法,它为给定的 Supplier<T> 返回一个 T 项。如果我们有一个 Supplier<List<String>>,其 get() 返回 List<String>,我们可以使用传统的匿名类来实现它:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Launcher {
public static void main(String[] args) {
Supplier<List<String>> listGenerator = new
Supplier<List<String>>() {
@Override
public List<String> get() {
return new ArrayList<>();
}
};
List<String> myList = listGenerator.get();
}
}
但我们也可以使用 lambda,它可以更简洁地实现 get() 并返回 List<String>,如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Launcher {
public static void main(String[] args) {
Supplier<List<String>> listGenerator = () -> new ArrayList<>
();
List<String> myList = listGenerator.get();
}
}
当你的 lambda 简单地使用 new 关键字在类型上调用构造函数时,你可以使用双冒号 :: lambda 语法来调用该类上的构造函数。这样,你可以省略 () 和 -> 符号,如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Launcher {
public static void main(String[] args) {
Supplier<List<String>> listGenerator = ArrayList::new;
List<String> myList = listGenerator.get();
}
}
RxJava 没有 Java 8 的 Supplier,而是有一个 Callable,它实现了相同的目的。
将 Consumer 实现为 lambda
Consumer<T> 接受一个 T 参数并使用它执行一个操作,但不返回任何值。使用匿名类,我们可以创建一个 Consumer<String>,它简单地打印字符串,如下面的代码片段所示:
import java.util.function.Consumer;
public class Launcher {
public static void main(String[] args) {
Consumer<String> printConsumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
printConsumer.accept("Hello World");
}
}
输出如下:
Hello World
你可以将其实现为一个 lambda。我们可以在 lambda 箭头 -> 的左侧选择调用 String 参数 s,然后在右侧打印它:
import java.util.function.Consumer;
public class Launcher {
public static void main(String[] args) {
Consumer<String> printConsumer = (String s) ->
System.out.println(s);
printConsumer.accept("Hello World");
}
}
编译器实际上可以根据你正在针对的 Consumer<String> 推断出 s 是一个 String 类型。因此,你可以省略那个显式的类型声明,如下面的代码所示:
import java.util.function.Consumer;
public class Launcher {
public static void main(String[] args) {
Consumer<String> printConsumer = s -> System.out.println(s);
printConsumer.accept("Hello World");
}
}
对于简单的单方法调用,实际上你可以使用另一种语法来使用双冒号 :: 声明 lambda。在双冒号的左侧声明你正在针对的类型,然后在双冒号的右侧调用其方法。编译器足够智能,可以推断出你试图将 String 参数传递给 System.out::println:
import java.util.function.Consumer;
public class Launcher {
public static void main(String[] args) {
Consumer<String> printConsumer = System.out::println;
printConsumer.accept("Hello World");
}
}
将 Function 实现为 lambda
Lambda 也可以实现接受参数并返回项的单个抽象方法。例如,RxJava 2.0(以及 Java 8)有一个 Function<T,R> 类型,它接受 T 类型并返回 R 类型。例如,你可以声明一个 Function<String,Integer>,其 apply() 方法将接受一个 String 并返回一个 Integer。在这里,我们通过在匿名类中返回字符串的长度来实现 apply(),如下所示:
import java.util.function.Function;
public class Launcher {
public static void main(String[] args) {
Function<String,Integer> lengthMapper = new Function<String,
Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
};
Integer length = lengthMapper.apply("Alpha");
System.out.println(length);
}
}
你可以通过使用 lambda 来使这个实现更加简洁,如下所示:
import java.util.function.Function;
public class Launcher {
public static void main(String[] args) {
Function<String,Integer> lengthMapper = (String s) ->
s.length();
Integer length = lengthMapper.apply("Alpha");
System.out.println(length);
}
}
我们有几个可选的语法可以用来实现 Function<String,Integer>。
Java 8 的编译器足够智能,可以根据我们分配给它的 Function<String,Integer> 类型推断出我们的参数 s 是一个 String 类型。因此,我们不需要显式声明 s 为 String 类型,因为它可以推断出来:
Function<String,Integer> lengthMapper = (s) -> s.length();
我们也不需要将 s 括在括号 (s) 中,因为对于单个参数来说,这些括号不是必需的(但如我们稍后看到的,对于多个参数来说,这些括号是必需的):
Function<String,Integer> lengthMapper = s -> s.length();
如果我们只是在传入的项目上调用方法或属性,我们可以使用双冒号 :: 语法来调用该类型上的方法:
Function<String,Integer> lengthMapper = String::length;
Function<T,R> 在 RxJava 中被广泛用作 Observable 操作符,通常用于转换发射的数据。最常用的例子是 map() 操作符,它将每个 T 发射转换为 R 发射,并从 Observable<T> 导出 Observable<R>:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha","Beta","Gamma")
.map(String::length) //accepts a Function<T,R>
.subscribe(s -> System.out.println(s));
}
}
注意,还有其他 Function 的变体,例如 Predicate 和 BiFunction,它们接受两个参数,而不是一个。reduce() 操作符接受一个 BiFunction<T,T,T>,其中第一个 T 参数是滚动聚合,第二个 T 是要放入聚合中的下一个项,第三个 T 是合并两个的结果。在这种情况下,我们使用 reduce() 来通过滚动总数添加所有项:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha","Beta","Gamma")
.map(String::length)
.reduce((total,next) -> total + next) //accepts a
BiFunction<T,T,T>
.subscribe(s -> System.out.println(s));
}
}
功能类型
在撰写本文时,以下是 RxJava 2.0 中所有可用的功能类型,您可以在 io.reactivex.functions 包中找到它们。您可能会认出其中许多功能类型几乎与 Java 8(在 java.util.function 中)或 Google Guava 中的类型相同。然而,它们在 RxJava 2.0 中被部分复制,以便在 Java 6 和 7 中使用。一个细微的区别是 RxJava 的实现会抛出检查异常。这消除了 RxJava 1.0 中的一个痛点,即检查异常必须在返回它们的 lambda 表达式中处理。
以下列出了 RxJava 1.0 的等效项,但请注意,单抽象方法(SAM)列对应于 RxJava 2.0 类型。RxJava 1.0 函数实现 call() 并不支持原始类型。RxJava 2.0 实现了一些带有原始类型的功能类型,以尽可能减少装箱开销:
| RxJava 2.0 | RxJava 1.0 | SAM | 描述 |
|---|---|---|---|
Action |
Action0 |
run() |
执行操作,类似于 Runnable |
Callable<T> |
Func0<T> |
get() |
返回单个类型为 T 的项 |
Consumer<T> |
Action1<T> |
accept() |
在给定的 T 项上执行操作,但不返回任何内容 |
Function<T,R> |
Func1<T,R> |
apply() |
接受类型 T 并返回类型 R |
Predicate<T> |
Func1<T,Boolean> |
test() |
接受 T 项并返回原始 boolean |
BiConsumer<T1,T2> |
Action2<T1,T2> |
accept() |
在 T1 和 T2 项上执行操作,但不返回任何内容 |
BiFunction<T1,T2,R> |
Func2<T1,T2,R> |
apply() |
接受 T1 和 T2 并返回类型 R |
BiPredicate<T1,T2> |
Func2<T1,T2,Boolean> |
test() |
接受 T1 和 T2 并返回原始 boolean |
Function3<T1,T2,T3,R> |
Func3<T1,T2,T3,R> |
apply() |
接受三个参数并返回 R |
BooleanSupplier |
Func0<Boolean> |
getAsBoolean() |
返回单个原始 boolean 值 |
LongConsumer |
Action1<Long> |
accept() |
在给定的 Long 上执行操作,但不返回任何内容 |
IntFunction |
Func1<T> |
apply() |
接受原始 int 并返回类型为 T 的项 |
在 RxJava 2.0 中,并非所有功能类型的原始等效项都已实现。例如,目前还没有像 Java 8 标准库中那样的 IntSupplier。这是因为 RxJava 2.0 不需要它来实现其任何操作符。
混合面向对象和响应式编程
当你开始将 RxJava 知识应用于现实世界的问题时,可能不会立即清楚的是如何将其与面向对象编程相结合。利用面向对象和函数式编程等多个范式正变得越来越普遍。在 Java 环境中,响应式编程和面向对象编程肯定可以为了更大的利益而协同工作。
显然,你可以从 Observable 或其他响应式类型中发射任何类型 T。发射基于你自己的类构建的对象是面向对象和响应式编程协同工作的一种方式。我们在本书中看到了许多例子。例如,Java 8 的 LocalDate 是一个复杂的面向对象类型,但你可以将它通过一个 Observable<LocalDate> 推送,如下面的代码所示:
import io.reactivex.Observable;
import java.time.LocalDate;
public class Launcher {
public static void main(String[] args) {
Observable<LocalDate> dates = Observable.just(
LocalDate.of(2017,11,3),
LocalDate.of(2017,10,4),
LocalDate.of(2017,7,5),
LocalDate.of(2017,10,3)
);
// get distinct months
dates.map(LocalDate::getMonth)
.distinct()
.subscribe(System.out::println);
}
}
输出如下:
NOVEMBER
OCTOBER
JULY
正如我们在本书中的几个例子中所看到的,许多 RxJava 操作符提供了适配器,可以将有状态的、面向对象的项目转换为响应式流。例如,有 generate() 工厂用于 Flowable 和 Observable,可以从一个可变对象构建一系列发射,该对象是逐步更新的。在下面的代码中,我们发射了一个无限连续的 Java 8 LocalDate 序列,但只取前 60 个发射。由于 LocalDate 是不可变的,我们将 2017-1-1 的种子 LocalDate 包裹在一个 AtomicReference 中,以便它可以被可变地替换为每次增量:
import io.reactivex.Emitter;
import io.reactivex.Flowable;
import java.time.LocalDate;
import java.util.concurrent.atomic.AtomicReference;
public class Launcher {
public static void main(String[] args) {
Flowable<LocalDate> dates =
Flowable.generate(() -> new AtomicReference<>
(LocalDate.of(2017,1,1)),
(AtomicReference<LocalDate> next, Emitter<LocalDate>
emitter) ->
emitter.onNext(next.getAndUpdate(dt ->
dt.plusDays(1)))
);
dates.take(60)
.subscribe(System.out::println);
}
}
输出如下:
2017-01-01
2017-01-02
2017-01-03
2017-01-04
2017-01-05
2017-01-06
...
因此,RxJava 有许多工厂和工具来适应你的面向对象、命令式操作,并将它们变为响应式。其中许多在本书中都有涉及。
但是,是否存在一个类从属性或方法返回 Observable、Flowable、Single 或 Maybe 的情况?当然有!当你的对象具有结果动态且随时间变化,并代表事件或大量数据序列的属性或方法时,它们是作为响应式类型返回的候选者。
这里有一个抽象的例子:比如说,你有一个表示飞行无人机的 DroneBot 类型。你可以有一个名为 getLocation() 的属性,它返回一个 Observable<Point> 而不是 Point。这样,你就可以获得一个实时流,每次无人机位置改变时都会推送一个新的 Point 发射:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
DroneBot droneBot = null; // create DroneBot
droneBot.getLocation()
.subscribe(loc ->
System.out.println("Drone moved to " + loc.x + "," +
loc.y));
}
interface DroneBot {
int getId();
String getModel();
Observable<Location> getLocation();
}
static final class Location {
private final double x;
private final double y;
Location(double x, double y) {
this.x = x;
this.y = y;
}
}
}
这个DroneBot示例展示了另一种有效混合面向对象和反应式编程的方式。你可以通过返回一个Observable来轻松获取该无人机的实时运动数据。这种模式有众多用例:股票行情、车辆位置、气象站数据、社交网络等等。然而,如果属性是无限的,就要小心了。如果你想要管理 100 个无人机的位置数据流,将它们所有的无限位置数据流扁平映射到一个单独的流中可能不会产生任何有意义的输出,除了一个没有上下文的位置噪声序列。你可能会分别订阅每一个,在一个 UI 中填充一个显示所有无人机的Location字段的表格,或者使用Observable.combineLatest()来发出所有无人机的最新位置快照。后者在实时显示地理地图上的点时可能很有帮助。
当类属性有限时,拥有反应式类属性也是有用的。比如说,你有一份仓库列表,并且想要计算所有仓库的总库存。每个Warehouse对象包含一个Observable<ProductStock>,它返回当前可用的产品库存的有限序列。ProductStock的getQuantity()方法返回该产品的可用数量。我们可以对getQuantity()值使用reduce()操作来获取所有可用库存的总和,如下所示:
import io.reactivex.Observable;
import java.util.List;
public class Launcher {
public static void main(String[] args) {
List<Warehouse> warehouses = null; // get warehouses
Observable.fromIterable(warehouses)
.flatMap(Warehouse::getProducts)
.map(ProductStock::getQuantity)
.reduce(0,(total,next) -> total + next)
.subscribe(i -> System.out.println("There are " + i + "
units in inventory"));
}
interface Warehouse {
Observable<ProductStock> getProducts();
}
interface ProductStock {
int getId();
String getDescription();
int getQuantity();
}
}
因此,像从Warehouse的getProducts()返回的有限Observable也有帮助,并且对于分析任务尤其有帮助。但请注意,这个特定的业务案例决定getProducts()将返回当时可用的产品,而不是每次库存变化时都广播库存的无限数据流。这是一个设计决策,有时,以冷数据的方式表示快照数据比热无限数据流更好。无限数据流将需要返回Observable<List<ProductStock>>(或Observable<Observable<ProductStock>>)以发出逻辑快照。你总是可以添加一个单独的Observable来发出更改通知,然后使用flatMap()在你的getProducts()上创建库存更改的热流。这样,你就在代码模型中创建了基本构建块,然后通过反应式地组合它们来完成更复杂的任务。
注意,你可以有返回反应式类型的方法并接受参数。这是一种创建针对特定任务定制的Observable或Flowable的强大方式。例如,我们可以在warehouse中添加一个getProductsOnDate()方法,该方法返回从给定日期发出的产品库存的Observable,如下面的代码所示:
interface Warehouse {
Observable<ProductStock> getProducts();
Observable<ProductStock> getProductsOnDate(LocalDate date);
}
总结来说,混合响应式和面向对象编程不仅有益,而且是必要的。在设计领域类时,仔细考虑哪些属性和方法应该做成响应式的,以及它们应该是冷、热和/或无限。想象一下你将如何使用你的类,以及你的候选设计是否容易或难以操作。确保不要为了响应式而将每个属性和方法都做成响应式的。只有当有可用性或性能优势时才将其做成响应式的。例如,你不应该将领域类型的 getId() 属性做成响应式的。这个类实例上的 ID 很可能不会改变,它只是一个单一值,不是一个值序列。
物质化和去物质化
我们没有涵盖的两个有趣的操作符是 materialize() 和 dematerialize()。我们没有在 第三章,“基本操作符”,中涵盖它们,因为那时在学习曲线上可能会造成混淆。但希望你在阅读这段内容时,已经足够了解 onNext()、onComplete() 和 onError() 事件,足以使用一个以不同方式抽象封装它们的操作符。
materialize() 操作符将这三个事件,onNext()、onComplete() 和 onError(),将它们全部转换为包装在 Notification<T> 中的发射项。所以如果您的源发出五个发射项,您将得到六个发射项,其中最后一个将是 onComplete() 或 onError()。在下面的代码中,我们使用五个字符串的 Observable 进行 materialize,这些字符串被转换成六个 Notification 发射项:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<String> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon");
source.materialize()
.subscribe(System.out::println);
}
}
输出结果如下:
OnNextNotification[Alpha]
OnNextNotification[Beta]
OnNextNotification[Gamma]
OnNextNotification[Delta]
OnNextNotification[Epsilon]
OnCompleteNotification
每个 Notification 有三个方法,isOnNext()、isOnComplete() 和 isOnError(),用于确定 Notification 是哪种类型的事件。还有一个 getValue() 方法,它将为 onNext() 返回发射值,但对于 onComplete() 或 onError() 将返回 null。我们利用 Notification 上的这些方法,如下面的代码所示,来过滤出三个事件到三个不同的观察者:
import io.reactivex.Notification;
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable<Notification<String>> source =
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon")
.materialize()
.publish()
.autoConnect(3);
source.filter(Notification::isOnNext)
.subscribe(n -> System.out.println("onNext=" +
n.getValue()));
source.filter(Notification::isOnComplete)
.subscribe(n -> System.out.println("onComplete"));
source.filter(Notification::isOnError)
.subscribe(n -> System.out.println("onError"));
}
}
输出结果如下:
onNext=Alpha
onNext=Beta
onNext=Gamma
onNext=Delta
onNext=Epsilon
onComplete
您也可以使用 dematerialize() 将发出通知的 Observable 或 Flowable 转换回正常的 Observable 或 Flowable。如果任何发射项不是 Notification,它将产生错误。不幸的是,在编译时,Java 无法强制执行应用于发出特定类型(如 Kotlin)的 Observables/Flowables 的操作符:
import io.reactivex.Observable;
public class Launcher {
public static void main(String[] args) {
Observable.just("Alpha", "Beta", "Gamma", "Delta",
"Epsilon")
.materialize()
.doOnNext(System.out::println)
.dematerialize()
.subscribe(System.out::println);
}
}
输出结果如下:
OnNextNotification[Alpha]
Alpha
OnNextNotification[Beta]
Beta
OnNextNotification[Gamma]
Gamma
OnNextNotification[Delta]
Delta
OnNextNotification[Epsilon]
Epsilon
OnCompleteNotification
那么,你到底会用 materialize() 和 dematerialize() 做什么呢?你可能不会经常使用它们,这也是为什么它们被放在附录中的另一个原因。但它们在组合更复杂的操作符和扩展转换器以执行更多操作而不从头创建低级操作符时非常有用。例如,RxJava2-Extras 使用 materialize() 来实现其操作符中的许多操作,包括 collectWhile()。通过将 onComplete() 视为一个发射事件,collectWhile() 可以将其映射到推动收集缓冲区到下游并开始下一个缓冲区。
否则,你可能不会经常使用它。但如果你需要用它来构建更复杂的转换器,了解它的存在是好的。
理解调度器
你可能不会单独使用这样的调度器,就像我们即将在本节中做的那样。你更有可能使用 observeOn() 和 subscribeOn()。但这里是如何在 Rx 上下文之外独立工作的。
调度器是 RxJava 对线程池和调度任务以由它们执行的概念抽象。这些任务可能根据其调用的执行方法立即执行、延迟执行或周期性重复执行。这些执行方法是 scheduleDirect() 和 schedulePeriodicallyDirect(),它们有几个重载。下面,我们使用计算调度器来执行一个立即任务、一个延迟任务和一个重复任务,如下所示:
import io.reactivex.Scheduler;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Scheduler scheduler = Schedulers.computation();
//run task now
scheduler.scheduleDirect(() -> System.out.println("Now!"));
//delay task by 1 second
scheduler.scheduleDirect(() ->
System.out.println("Delayed!"), 1, TimeUnit.SECONDS);
//repeat task every second
scheduler.schedulePeriodicallyDirect(() ->
System.out.println("Repeat!"), 0, 1, TimeUnit.SECONDS);
//keep alive for 5 seconds
sleep(5000);
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
你的输出可能如下所示:
Now!
Repeat!
Delayed!
Repeat!
Repeat!
Repeat!
Repeat!
Repeat!
scheduleDirect() 将只执行一次任务,并接受可选的重载来指定时间延迟。schedulePeriodicallyDirect() 将无限重复。有趣的是,所有这些方法都返回一个 Disposable,以便取消正在执行或等待执行的任务。
这三种方法会自动将任务传递给一个 Worker,这是一个抽象,它围绕一个单线程进行工作,该线程按顺序执行分配给它的任务。你实际上可以调用调度器的 createWorker() 方法来显式获取一个 Worker 并直接将任务委托给它。它的 schedule() 和 schedulePeriodically() 方法与调度器的 scheduleDirect() 和 schedulePeriodicallyDirect() 方法操作类似(并且也返回可取消的),但它们是由指定的 Worker 执行的。当你完成一个 Worker 时,你应该取消它,以便它可以被丢弃或返回到 Scheduler。以下是我们之前示例使用 Worker 的等效示例:
import io.reactivex.Scheduler;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.TimeUnit;
public class Launcher {
public static void main(String[] args) {
Scheduler scheduler = Schedulers.computation();
Scheduler.Worker worker = scheduler.createWorker();
//run task now
worker.schedule(() -> System.out.println("Now!"));
//delay task by 1 second
worker.schedule(() -> System.out.println("Delayed!"), 1,
TimeUnit.SECONDS);
//repeat task every second
worker.schedulePeriodically(() ->
System.out.println("Repeat!"), 0, 1, TimeUnit.SECONDS);
//keep alive for 5 seconds, then dispose Worker
sleep(5000);
worker.dispose();
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
你可能会得到以下输出:
Now!
Repeat!
Repeat!
Delayed!
Repeat!
Repeat!
Repeat!
Repeat!
当然,每个调度器的实现方式都不同。一个调度器可能使用一个线程或多个线程。它可能缓存和重用线程,也可能完全不重用它们。它可能使用 Android 线程或 JavaFX 线程(正如我们在本书中看到的 RxAndroid 和 RxJavaFX)。但这就是调度器的基本工作方式,也许你可以看到为什么它们在实现 RxJava 操作符时很有用。


浙公网安备 33010602011771号