1. 概述
RxJava 提供了丰富的操作符,用于将 Observable 发出的数据项转换为新的 Observable。其中,flatMap
和 switchMap
是两个非常常用但也容易混淆的操作符,尤其对于刚接触响应式编程的开发者来说,它们之间的差异常常让人摸不着头脑。
如果你还不熟悉 RxJava 的基本概念,建议先阅读 RxJava 入门指南。
本文通过一个简单但贴近实际的场景,帮你彻底搞清楚 flatMap
和 switchMap
的核心区别,避免在项目中踩坑。
2. flatMap:扁平化合并,保留所有结果 ✅
flatMap
的作用是:将源 Observable 中的每个数据项,通过一个函数映射成一个新的 Observable,然后将这些 Observable 的结果“扁平化”合并到一个最终的 Observable 中,所有结果都会被保留。
关键点:
- 所有中间 Observable 都会被订阅
- 结果是所有子 Observable 发出的数据的合集
- 发出顺序不保证与原始顺序一致(异步环境下常见)
场景示例:搜索建议(每输入一个字符都请求一次)
假设我们模拟一个搜索框的输入过程,用户每输入一个字符,就发起一次搜索请求。我们希望看到所有请求的结果都被返回,不管后续有没有新的输入。
为了简化,我们将输入序列定义为一个字符串列表:
// given
List<String> actualOutput = new ArrayList<>();
TestScheduler scheduler = new TestScheduler();
List<String> keywordToSearch = Arrays.asList("b", "bo", "boo", "book", "books");
// when
Observable.fromIterable(keywordToSearch)
.flatMap(s -> Observable.just(s + " FirstResult", s + " SecondResult")
.delay(10, TimeUnit.SECONDS, scheduler))
.toList()
.doOnSuccess(s -> actualOutput.addAll(s))
.subscribe();
scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
// then
assertThat(actualOutput, hasItems("b FirstResult", "b SecondResult",
"boo FirstResult", "boo SecondResult",
"bo FirstResult", "bo SecondResult",
"book FirstResult", "book SecondResult",
"books FirstResult", "books SecondResult"));
✅ 输出结果包含所有输入项对应的结果,共 10 条。
⚠️ 注意:由于 flatMap
是并发执行的(每个映射的 Observable 独立运行),最终输出顺序可能每次都不一样,RxJava 不保证顺序一致性。
3. switchMap:只保留最新结果,取消旧请求 ✅
switchMap
的行为与 flatMap
看似相似,但有一个致命区别:
每当源 Observable 发出新数据时,
switchMap
会取消前一个映射出的 Observable 的订阅,并只订阅最新的那个。
换句话说:只保留最新一次请求的结果,之前的请求即使还没完成,也会被丢弃。
场景示例:搜索优化(只关心最终关键词)
在实际搜索场景中,用户快速输入时,我们并不关心中间的 "b"
、"bo"
这些短词的搜索结果,只希望拿到最终 "books"
的结果。这时,switchMap
就是最佳选择。
我们只需将上面例子中的 flatMap
替换为 switchMap
:
// given
List<String> actualOutput = new ArrayList<>();
TestScheduler scheduler = new TestScheduler();
List<String> keywordToSearch = Arrays.asList("b", "bo", "boo", "book", "books");
// when
Observable.fromIterable(keywordToSearch)
.switchMap(s -> Observable.just(s + " FirstResult", s + " SecondResult")
.delay(10, TimeUnit.SECONDS, scheduler))
.toList()
.doOnSuccess(s -> actualOutput.addAll(s))
.subscribe();
scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
// then
assertEquals(2, actualOutput.size());
assertThat(actualOutput, hasItems("books FirstResult", "books SecondResult"));
✅ 最终结果只有 2 条,且只包含 "books"
的搜索结果。
❌ 之前的 "b"
、"bo"
等请求的结果全部被取消,不会出现在最终输出中。
💡 这正是 switchMap
的核心价值:避免处理过时数据,减少无效网络请求和 UI 更新,提升性能和用户体验。
4. 总结对比
特性 | flatMap |
switchMap |
---|---|---|
是否保留所有结果 | ✅ 是 | ❌ 否,只保留最新 |
是否取消旧订阅 | ❌ 否 | ✅ 是,自动取消 |
适用场景 | 需要处理所有请求结果(如批量任务) | 只关心最新状态(如搜索、防抖请求) |
并发行为 | 所有子 Observable 并发执行 | 只执行最新的,旧的被中断 |
输出顺序 | 不保证 | 取决于最新 Observable 的顺序 |
简单粗暴记忆法:
flatMap
:“全都要” —— 所有请求都处理,结果全返回switchMap
:“只认最新” —— 后到的请求会“踢掉”前面的
实际开发建议
- 在搜索框、自动补全、实时查询等场景,优先使用
switchMap
,避免 UI 闪烁和资源浪费 - 在需要聚合多个独立请求结果的场景(如并行拉取多个配置),使用
flatMap
- 如果你还担心顺序问题,可以考虑
concatMap
(顺序执行,不并发)
所有示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/rxjava-modules/rxjava-core
建议 clone 下来跑一遍,印象更深刻。