这是我参与新手入门的第2篇文章。
初识
自从 Java 8 加了 Stream API以后,流操作就成了代码中一道亮丽的风景线。
一不小心,我们的代码可能就写成了下面这样子:
map
.entrySet()
.stream()
.sorted((a, b) -> Long.compare(
a.getValue().values().stream().mapToLong(l -> l).sum(),
b.getValue().values().stream().mapToLong(l -> l).sum()))
.collect(Collectors.toList());
复制代码
上面的代码是我从线上真实环境截取的一个代码片段,从直观上看,它是不是像玩贪吃蛇游戏一样,一点点地拼装逻辑代码?
Stream 是什么
Java 8 Stream 不应与 Java I/O 流(例如:FileInputStream 等)混淆,这些彼此之间几乎没有关系。
流是数据源的包装器,允许我们使用该数据源进行操作并使批量处理变得方便快捷。流不存储数据,从这个意义上说,它不是数据结构,它也从不修改底层数据源。
你可以简单地理解,Stream 更像是对集合 Collection 提供的增强操作,它可以方便高效地进行各种筛选和聚合操作,也适用于数据量较大的数据处理。
当我们使用一个流的时候,通常包括三个基本步骤:
获取流(stream)→ 数据转换(transform)→执行操作(ending) 获取想要的结果。每次转换,原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,一直延伸下去。
当然,Stream 也不是孤立出现的,要想让它成为自己编程的利器,还需要对 lambda 表达式、Optional、方法引用等有一定了解。
获取 Stream
可以有很多种方式获取流,不过流的创建是不影响原始数据的,因此可以对同一数据获取多个不同的 Stream 实例。
利用 Stream 对象
最简单的方式是通过 Stream 获取流。
Stream<String> stream = Stream.of("a", "b", "c");
复制代码
正如上面的代码,获取了一个包含字符串 “a”、“b”、“c”的流。
还可以通过数据的方式获取:
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
复制代码
二者可以达到相同的效果。
利用迭代器
使用 iterate()
方法可以创建无限流:
Stream<Integer> streamIterated = Stream.iterate(10, n -> n + 2).limit(5);
复制代码
迭代器的功能是创建每个后续元素时,指定的函数将应用于前一个元素。在上面的例子中,将创建 10,12,14,16,18 一共5条数据。如果不指定 limit()
,程序将一直执行下去。
利用集合
在 Java 8 中, stream() 方法被加入到了 Collection 集合中,也就是说,集合框架拥有完整的 stream 流操作。
private static List<Employee> tempList = Arrays.asList(arrayOfEmps);
tempList.stream();
复制代码
上述代码的第二行,简单直接创建了流。这也是在实际开发中最常用的创建 stream 方式。
中间操作
一个流可以后面跟随零个或多个 中间操作(intermediate)。其目的主要是打开流,做出某种程度的数据映射、过滤和筛选,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并不会真正开始流的遍历。
流的中间操作是可以列举的:
- map 、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
遇到上述操作,就说明后面还需要进一步做数据处理。
map 数据转换
map
的作用是对流中每一条数据做数据映射/转换。
List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());
复制代码
上述例子中第二行就是利用 map 对 wordList 中所有数据转大写。
filter 数据过滤
filter
是 stream api 中最常用的一个,它的作用类似一个 if 判断子句,但是看起来清晰了很多。
List<String> elements =
Stream.of("a", "b", "c")
.filter(element -> element.contains("b"))
.collect(Collectors.toList());
复制代码
过滤包含 “b” 元素的数据。
中间操作包含了最重要数据筛选逻辑,实际使用时,这部分代码也占据了绝对数量的比重,因此是必须掌握的内容。
终止操作
一个流只能有一个终止操作(terminal),当该操作执行后,流就被使用“尽”了,无法再被操作。所以这必定是流的最后一个操作。终止操作的执行,才会真正开始流的遍历,并且会生成一个结果。
流的终止操作是也可以列举的:
- forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
在调用晚上述 api 后,不能对流任何形式的 stream 操作,否则将触发 IllegalStateException
。
forEach
forEach
和传统意义上 for 含义几乎一致,它们仅仅表现在书写形式的不同。forEach
方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。
//降序,默认是升序
users.stream().sorted(Comparator.comparing(User::getId).reversed()).forEach(System.out::println);
复制代码
上面的代码通过对 users 排序,然后将结果输出到控制台。
需要注意的是,forEach
不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。
collect 将流转换为集合
collect
的用途非常广泛,一般与之一起用的 Collectors
极大的增加了 collect 的易用性。这也使得 collect 是终止操作中最重要的一个。
List<String> collectorCollection =
productList.stream().map(Product::getName).collect(Collectors.toList());
复制代码
上面的代码可以将对象转换为 list。
Map<Long, String> map = users.stream().collect(Collectors.toMap(p -> p.getId(), p -> p.getName()));
复制代码
上述代码可以将 list 转换为 map。
短路操作
在 stream api 中有一些被称之为 Short-circuiting
,它们可以在出现无限流时提前终止。可以认为它们是一类特殊的终止操作。
- anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
上面出现过一个场景,产生偶数:
Stream.iterate(10, n -> n + 2).limit(5);
如果不利用 limit 短路,程序无法停止。
总结
- Stream 不会改变原始数据,如果需要修改数据,会产生一个新的数据集
- Stream 操作分3步,获取流,中间操作,终止操作,它们的顺便不能变
- 很多 Stream 操作是向后延迟的,是惰性化的
- Stream 可以是无限的,此时需要短路操作来提前终止执行
最后用一张图总结一下 stream 操作。