Java Stream 流操作——像玩贪吃蛇一样写代码

这是我参与新手入门的第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 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,一直延伸下去。

871373-20170806212158787-89103045_副本.png

当然,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 可以是无限的,此时需要短路操作来提前终止执行

Snipaste_20210707_22-34-03_副本.png

最后用一张图总结一下 stream 操作。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享