Java8实战-总结16
- 引入流
- 流与集合
- 只能遍历一次
- 外部迭代与内部迭代
引入流
流与集合
只能遍历一次
和迭代器类似,流只能遍历一次。遍历完之后,这个流就已经被消费掉了。可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O
通道就没戏了)。例如,以下代码会抛出一个异常,说流已被消费掉了:
List<String> title = Arrays.asList("Java8","In","Action");
Stream<String> s = title.stream();
//打印标题中的每个单词
s.forEach(System.out::println);
//java.lang.IllegalStateException:流已被操作或关闭
s.forEach(System.out::println);
所以要记得,流只能消费一次。
对于喜欢哲学的读者,你可以把流看作在时间中分布的一组值。相反,集合则是空间(这里就是计算机内存)中分布的一组值,
在一个时间点上全体存在——你可以使用迭代器来访问for-each循环中的内部成员。
集合和流的另一个关键区别在于它们遍历数据的方式。
外部迭代与内部迭代
使用Collection
接口需要用户去做迭代(比如用for-each
),这称为外部迭代。相反,Streams
库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。下面的代码列表说明了这种区别:
//集合:用for-each循环外部迭代
List<String> names = new ArrayList<>();
for(Dish d : menu) { //显式顺序迭代菜单列表
names.add(d.getName());//提取名称并将其添加到累加器
}
请注意,for-each
还隐藏了迭代中的一些复杂性。for-each
结构是一个语法糖,它背后的东西用Iterator
对象表达出来更要丑陋得多。
//集合:用背后的迭代器做外部迭代
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {//显式迭代
Dish d = iterator.next();
names.add(d.getName());
}
//流:内部迭代
List<String> names = menu.stream()
.map(Dish::getName)//用getName方法参数化map,提取菜名
.collect(toList());//开始执行操作流水线;没有迭代!
用一个比喻来解释内部迭代的差异和好处吧。比方说你在和索菲亚说话,希望她能把玩具收起来。
你:“索菲亚,我们把玩具收起来吧。地上还有玩具吗?”
索菲亚:“有,球。”
你:“好,把球放进盒子里。还有吗?”
索菲亚:“有,那是我的娃娃。”
你:“好,把娃娃放进盒子里。还有吗?”
索菲亚:“有,有我的书。”
你:“好,把书放进盒子里。还有吗?”
索菲亚:"没了,没有了。"
你:“好,我们收好啦。”
这正是每天都要对Java集合做的。外部迭代一个集合,显式地取出每个项目再加以处理。如果只需跟索菲亚说“把地上所有的玩具都放进盒子里”就好了。内部迭代比较好的原因有二:
第一,索非亚可以选择一只手拿娃娃,另一只手拿球;第二,她可以决定先拿离盒子最近的那个东西,然后再拿别的。同样的道理,内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理。要是用Java
过去的那种外部迭代方法,这些优化都是很困难的。这似乎有点儿鸡蛋里挑骨头,但这差不多就是Java8
引入流的理由了——Streams
库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。与此相反,一旦通过写for-each
而选择了外部迭代,那基本上就要自己管理所有的并行问题了(自己管理实际上意味着“某个良辰吉日我们会把它并行化”或“开始了关于任务和synchronized
的漫长而艰苦的斗争”)。Java 8
需要一个类似于Collection
却没有迭代器的接口,于是就有了stream
。下图说明了流(内部迭代)与集合(外部迭代)之间的差异:
内部迭代与外部迭代
已经说过了集合与流在概念上的差异,特别是流利用了内部迭代:替你把迭代做了。但是,只有已经预先定义好了能够隐藏迭代的操作列表,例如filter
或map
,这个才有用。大多数这类操作都接受Lambda
表达式作为参数,因此可以用前面介绍的方法来参数化其行为。Java
语言的设计者给Stream API
配上了一大套可以用来表达复杂数据处理查询的操作。