Stream 和 Collection 的区别是什么
流和集合的区别是什么?
粗略地说, 集合和流之间的差异就在于什么时候进行计算集合是一个内存中的数据结构, 它包含数据结构中目前所有的值 -- 集合中的每个元素都得先计算出来才能添加到内存里(你可以往集合里加东西或者删东西, 但是不管什么时候, 集合中的每个元素都是放在内存里的, 元素都得计算出来才能成为集合的一部分)
相比之下, 流则是在概念上固定的数据结构(你不能添加或者删除元素), 其元素则是按需计算的这对编程有很大的好处用户仅仅从流中提取需要的值, 而这些值 -- 在用户看不见的地方 -- 只会按需生成这是一种生产者 - 消费者的关系从另一个角度来说, 流就像一个延迟创建的集合: 只有在消费者要求的时候才会计算值
Stream 是内部迭代
一个明显的区别是迭代方式不同 Collection 需要手动 for-each 或者使用 Iterator 在外部迭代而 Stream 则开启后可以直接对单个元素进行操作, 内部帮你做好了迭代工作
内部迭代的好处是可一个更好的并行自己手写迭代需要处理好每次迭代的内容为了提高执行效率, 也许会把多个处理逻辑写到同一个遍历里比如, 有同事看到从 scala 转过来的同事的代码, 说他写的代码经常重复好多次 scala 是函数式语言, 和流天然集成而我们惯性的做法, 还是把一堆操作逻辑写到同一个循环体中, 来满足自己对所谓的性能要求的洁癖这常常会使得可读性变差很厌烦阅读超过 100 行的代码, 尤其代码还有首尾同步处理的逻辑(for, try-catch), 很容易出错多写一次循环来做这些事情, 心理又过不去
Stream 开启流之后, 系统内部会分析对元素的操作是否可以并行, 然后合并执行也就是说, 看起来, 自己
filter-map-filter-map-group
很多次, 但真实执行的时候并不是遍历了很多次至于到底遍历了多少次这是一个好问题, 后面会说明这个问题
使用流 Stream 的注意事项
流只能消费一次比如, foreach 只能遍历一次 stream 再次则会抛异常
流操作
针对流的操作方式两种:
中间操作
可以连接起来的流操作叫做中间操作诸如 filter 或 map 等中间操作会返回另一个流这让多个操作可以连接起来形成一个查询但是, 除非调用一个终端操作, 比如 collect,foreach, 否则中间操作不会执行 ---- 它们很懒这是因为中间操作一般可以合并起来, 在终端操作时一次性全部处理
终端操作
关闭流的操作叫做终端操作终端操作会从流的流水线生成结果
使用流
本文 demo 源码: https://github.com/Ryan-Miao/someTest/tree/master/src/main/java/com/test/java8/streams
新建一个 Entity 作为基本元素
- package com.test.java8.streams.entity;
- /**
- * Created by Ryan Miao on 12/11/17.
- */
- public class Dish {
- private final String name;
- private final boolean vegetarian;
- private final int calories;
- private final Type type;
- public Dish(String name, boolean vegetarian, int calories, Type type) {
- this.name = name;
- this.vegetarian = vegetarian;
- this.calories = calories;
- this.type = type;
- }
- public String getName() {
- return name;
- }
- public boolean isVegetarian() {
- return vegetarian;
- }
- public int getCalories() {
- return calories;
- }
- public Type getType() {
- return type;
- }
- public enum Type {
- MEAT,
- FISH,
- OTHER
- }
- }
最长用, 最简单的用法
Stream API 支持许多操作, 这些操作能让你快速完成复杂的数据查询, 比如筛选切片映射查找匹配和归约
- package com.test.java8.streams;
- import com.google.common.collect.Lists;
- import com.test.java8.streams.entity.Dish;
- import org.junit.Before;
- import org.junit.Test;
- import java.util.List;
- import static java.util.stream.Collectors.toList;
- /**
- * Created by Ryan Miao on 12/11/17.
- */
- public class StreamExample {
- private List<Dish> menu;
- @Before
- public void setUp(){
- menu = Lists.newArrayList(
- new Dish("pork", false, 800, Dish.Type.MEAT),
- new Dish("beef", false, 700, Dish.Type.MEAT),
- new Dish("chicken", false, 400, Dish.Type.MEAT),
- new Dish("french fries", true, 530, Dish.Type.OTHER),
- new Dish("rice", true, 350, Dish.Type.OTHER),
- new Dish("season fruit", true, 120, Dish.Type.OTHER),
- new Dish("pizza", true, 550, Dish.Type.OTHER),
- new Dish("prawns", false, 300, Dish.Type.FISH),
- new Dish("salmon", false, 450, Dish.Type.FISH)
- );
- }
- @Test
- public void demo(){
- List<String> threeHighCaloricDishNames = menu.stream()
- .filter(dish -> dish.getCalories()> 300)
- .map(Dish::getName)
- .limit(3)
- .collect(toList());
- System.out.println(threeHighCaloricDishNames);
- }
- }
stream()将一个集合转换成一个流, 流和 list 一样, 都是单元素的集合体
filter()接受一个布尔值 lambda, 即一个谓词当表达式的 value 是 true 的时候, 该元素通过筛选
map()接受一个转换 lambda, 将一个元素 class 映射成另一个 class
collect 收集器, 汇总结果, 触发流, 终端操作
谓词筛选 filter
谓词是一个返回 boolean 的函数, 也就是条件, 通过这个条件进行筛选
- @Test
- public void testFilterMapLimit(){
- List<Entity> entities = Lists.newArrayList(new Entity(100), new Entity(12), new Entity(33), new Entity(41));
- List<Integer> collect = entities.stream()
- .filter(entity -> entity.getId() <100)
- .map(Entity::getId)
- .collect(Collectors.toList());
- System.out.println(collect);
- }
这里, filter 的参数就是一个谓词, 配合 filter, 可以筛选结果, 只有返回值是 true 的 item 会通过
去重复 distinct
distinct()
截短流 limit
limit(n)
跳过元素 skip
skip(n) 通过 limit(n)形成互补关系
映射 map
map, stream 的核心操作接收一个参数, 用来把一个对象转换为另一个 demo 同上下面看具体需求
- /**
- * Returns a stream consisting of the results of applying the given
- * function to the elements of this stream.
- *
- * <p>This is an <a href="package-summary.html#StreamOps">intermediate
- * operation</a>.
- *
- * @param <R> The element type of the new stream
- * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
- * <a href="package-summary.html#Statelessness">stateless</a>
- * function to apply to each element
- * @return the new stream
- */
- <R> Stream<R> map(Function<? super T, ? extends R> mapper);
接收一个 Function 函数, 然后返回 Stream. 而 Function 在前面已经介绍过了我们看核心的方法
- /**
- * Represents a function that accepts one argument and produces a result.
- *
- * <p>This is a <a href="package-summary.html">functional interface</a>
- * whose functional method is {@link #apply(Object)}.
- *
- * @param <T> the type of the input to the function
- * @param <R> the type of the result of the function
- *
- * @since 1.8
- */
- @FunctionalInterface
- public interface Function<T, R> {
- /**
- * Applies this function to the given argument.
- *
- * @param t the function argument
- * @return the function result
- */
- R apply(T t);
- }
Function 函数的功能就是把参数转换成另一个类型的对象, 返回也就是 a -> {return b;}
瞥一眼 Peek
上面 map 的需求特别多, 但有时候我并不想返回另一个对象, 我只是想要把原来的对象加工一个下, 还是返回原来的对象用 map 也是可以的, 只要返回同一个对象就行但 IDEA 会推荐用 peek()
比如, 我想把 list 的 user 全部取出来, 把 updateDate 更新为当前时间
- @Test
- public void testPeek(){
- final List<Integer> list = Lists.newArrayList(1,2,3,4);
- List<Entity> collect = list.stream()
- .map(Entity::new)
- .peek(e -> e.setUpdateTime(new Date()))
- .collect(Collectors.toList());
- System.out.println(collect);
- }
源码里是这样写的
- /**
- * Returns a stream consisting of the elements of this stream, additionally
- * performing the provided action on each element as elements are consumed
- * from the resulting stream.
- *
- * <p>This is an <a href="package-summary.html#StreamOps">intermediate
- * operation</a>.
- *
- * <p>For parallel stream pipelines, the action may be called at
- * whatever time and in whatever thread the element is made available by the
- * upstream operation. If the action modifies shared state,
- * it is responsible for providing the required synchronization.
- *
- * @apiNote This method exists mainly to support debugging, where you want
- * to see the elements as they flow past a certain point in a pipeline:
- * <pre>{@code
- * Stream.of("one", "two", "three", "four")
- * .filter(e -> e.length()> 3)
- * .peek(e -> System.out.println("Filtered value:" + e))
- * .map(String::toUpperCase)
- * .peek(e -> System.out.println("Mapped value:" + e))
- * .collect(Collectors.toList());
- * }</pre>
- *
- * @param action a <a href="package-summary.html#NonInterference">
- * non-interfering</a> action to perform on the elements as
- * they are consumed from the stream
- * @return the new stream
- */
- Stream<T> peek(Consumer<? super T> action);
而 Consumer 同样也在之前出现过
- @FunctionalInterface
- public interface Consumer<T> {
- /**
- * Performs this operation on the given argument.
- *
- * @param t the input argument
- */
- void accept(T t);
- }
也就是说, peek()的本意是将对象取出来, 消一遍, 并不是像我的说的那样返回原对象, 因为参数并不是 Function, 而是 Consumer 我之所以这么说是因为 Function 也可以做到这个功能, 只要将返回值变为当前对象即可而 peek 里, 我们可以修改当前对象的属性, 也是会生效的
流的扁平化 faltMap
我们前面讲的函数都是处理一个序列, 一个 list, 一个 Stream 里的数据如果一个 Stream 的元素也是另一个 stream 呢? 我还想把这个 Stream 的元素的 stream 打散, 最终输出一个 stream 比如下面这个例子统计单词列表中出现的字母
- final List<String> words = Lists.newArrayList( "Hello", "worlds");
- List<String[]> rs = words.stream()
- .map(w -> w.split(""))
- .distinct()
- .collect(Collectors.toList());
- rs.forEach(e -> {
- for (String i : e) {
- System.out.print(i + ",");
- }
- System.out.println();
- });
打印的结果为:
- H,e,l,l,o,
- w,o,r,l,d,s,
显然, 目标没达到 map 之后的 stream 已经变成
Stream<Stream<String>>
应该如何把里面的 Stream 打开, 最后拼接起来呢最直观的想法就是用一个新的 list, 将我们刚才 foreach 打印的步骤中的操作变成插入 list 但这显然不是函数式编程
flatMap 可以接收一个参数, 返回一个流, 这个流可以拼接到最外层的流说的太啰嗦, 看具体用法
- @Test
- public void flatMap() {
- final List<String> words = Lists.newArrayList( "Hello", "worlds");
- List<String> collect = words.stream()
- .map(w -> w.split(""))
- .flatMap(a -> Arrays.stream(a))
- .distinct()
- .collect(Collectors.toList());
- System.out.println(collect);
- }
第一步, 用 map 将一个 String 对象映射成 String[]数组
第二步, 将这个返回的对象映射成 Stream, 这里的数组转 Stream 即 Arrays::stream.
第三步, 用 flatMap
以上可以合并为一步:
.flatMap(w -> Arrays.stream(w.split("")))
最终打印结果:
[H, e, l, o, w, r, d, s]
查找和匹配
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性 Stream API 通过 allMatch, anyMatch,noneMatch,findFirst,findAny 方法提供了这样的工具
比如, 找到任何一个匹配条件的
- @Test
- public void anyMatchTest() {
- final List<Entity> entities = Lists.newArrayList(new Entity(101),
- new Entity(12), new Entity(33), new Entity(42));
- boolean b = entities.stream().anyMatch(e -> {
- System.out.println(e.getId());
- return e.getId() % 2 == 0;
- });
- if (b) {
- System.out.println("有偶数");
- }
- }
- 101
- 12
有偶数
上述只是确定下是不是存在, 在很多情况下这就够了至于 FindAny 和 FindFirst 则是找到后返回, 目前还没遇到使用场景
归约 Reduce
Google 搜索提出的 Map Reduce 模型, Hadoop 提供了经典的开源实现在 Java 中, 我们也可以手动实现这个
reduce 的操作在函数式编程中很常见, 作用是将一个历史值与当前值做处理比如求和, 求最大值
求和的时候, 我们会将每个元素累加给 sum 用 reduce 即可实现:
- /**
- * 没有初始值, 返回 Optional
- */
- @Test
- public void demo(){
- OptionalInt rs = IntStream.rangeClosed(1, 100)
- .reduce((left, right) -> {
- System.out.println(left + "\t" + right);
- return left + right;
- });
- if (rs.isPresent()){
- System.out.println("===========");
- System.out.println(rs.getAsInt());
- }
- }
打印结果为:
- 1 2
- 3 3
- 6 4
- ...
- ...
- 4851 99
- 4950 100
- ===========
- 5050
给一个初始值
- int rs = IntStream.rangeClosed(1, 100)
- .reduce(10, (a, b) -> a + b);
同样, 可以用来求最大值
- List<Integer> nums = Lists.newArrayList(3, 1, 4, 0, 8, 5);
- Optional<Integer> max = nums.stream().reduce((a, b) -> b> a ? b : a);
这里的比较函数恰好是 Integer 的一个方法, 为增强可读性, 可以替换为:
nums.stream().reduce(Integer::max).ifPresent(System.out::println);
接下来, 回归我们最初的目标, 实现伟大的 Map-Reduce 模型比如, 想要知道有多少个菜(一个 dish list)
- @Test
- public void mapReduce() {
- final ArrayList<Dish> dishes = Lists.newArrayList(
- new Dish("pork", false, 800, Type.MEAT),
- new Dish("beef", false, 700, Type.MEAT),
- new Dish("chicken", false, 400, Type.MEAT),
- new Dish("french fries", true, 530, Type.OTHER),
- new Dish("rice", true, 350, Type.OTHER),
- new Dish("season fruit", true, 120, Type.OTHER),
- new Dish("pizza", true, 550, Type.OTHER),
- new Dish("prawns", false, 300, Type.FISH),
- new Dish("salmon", false, 450, Type.FISH)
- );
- Integer sum = dishes.stream()
- .map(d -> 1)
- .reduce(0, (a, b) -> a + b);
- }
归约的优势和并行化
相比于用 foreach 逐步迭代求和, 使用 reduce 的好处在于, 这里的迭代被内部迭代抽象掉了, 这让内部实现得以选择并行执行 reduce 操作而迭代式求和例子要更新共享变量 sum, 这不是那么容易并行化的如果你加入了同步, 很可能会发现线程竞争抵消了并行本应带来的性能提升! 这种计算的并行化需要另一种方法: 将输入分块, 分块求和, 最后再合并起来但这样的话代码看起来就完全不一样了后面会用分支 / 合并框架来做这件事但现在重要的是要认识到, 可变的累加模式对于并行化来说是死路一条你需要一种新的模式, 这正是 reduce 所提供的传递给 reduce 的 lambda 不能更改状态(如实例变量), 而且操作必须满足结合律才可以按任意顺序执行
流操作的状态: 无状态和有状态
你已经看到了很多的流操作, 乍一看流操作简直是灵丹妙药, 而且只要在从集合生成流的时候把 Stream 换成 parallelStream 就可以实现并行但这些操作的特性并不相同他们需要操作的内部状态还是有些问题的
诸如 map 和 filter 等操作会从输入流中获取每一个元素, 并在输出流中得到 0 或 1 个结果这些操作一般是无状态的: 他们没有内部状态(假设用户提供的 lambda 或者方法引用没有内部可变状态)
但诸如 reducesummax 等操作需要内部状态来累积结果在前面的情况下, 内部状态很小在我们的例子里就是一个 int 或者 double 不管流中有多少元素要处理, 内部状态都是有界的
相反, 诸如 sort 或 distinct 等操作一开始都和 filter 和 map 差不多 -- 都是接受一个流, 再生成一个流 (中间操作), 但有一个关键的区别从流中排序和删除重复项都需要知道先前的历史例如, 排序要求所有元素都放入缓冲区后才能给输出流加入一个项目, 这一操作的存储要求是无界的要是流比较大或是无限的, 就可能会有问题(把质数流倒序会做什么呢? 它应当返回最大的质数, 但数学告诉我们他不存在) 我们把这些操作叫做有状态操作
注
以上内容均来自 Java8 In Action
来源: https://www.cnblogs.com/woshimrf/p/java8-stream1.html