Stream One

只要是做Java开发的人,相信没有没用过集合的。而且通常我们使用集合的标准步骤,一个for或者while循环,然后迭代处理里面的每一个对象。例如像这样:

// 从一个球队集合中过滤出积分大于50的俱乐部
List<SoccerTeam> teams = Lists.newArrayList();// 一个球队集合
List<SoccerTeam> gt50ScoreClubs = Lists.newArrayList();// 积分大于50的俱乐部
for (SoccerTeam team : teams) {
    if (team.isClub() && team.getScore() > 50) {
        gt50ScoreClubs.add(team);
    }
}

在这段代码中,gt50ScoreClubs是一个中间容器,目的是为了装入过滤后的结果,而过滤的逻辑和循环也是耦合在一起,特别在工作中开发时,一个循环中经常会同时处理几个业务逻辑,使得看代码的人,常常搞不懂里面到底做了什么。进入Java8之后,你就有了另一种处理集合的形式。

// 使用流来处理集合
List<SoccerTeam> gt50ScoreClubs = teams.stream().filter(SoccerTeam::isClub)
                .filter(team -> team.getScore() > 50)
                .collect(Collectors.toList());

流是什么

是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不 是临时编写一个实现),你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

如何使用流
一般使用流分为三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。

中间操作是对数据的处理,通常会返回另一个流,因此可以将多个中间操作拼接在一起,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。 这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。例如:

List<String> numStrings = Lists.newArrayList("1""15""6""10""9",
                "11""9""13""15");
// 一下全是中间操作,再触发一个终端操作之前,都不会被执行
Stream<Integer> stream = numStrings.stream().map(Integer::parseInt)
                .filter(num -> num % 2 == 1).distinct().sorted().limit(3);
// 触发终端操作
stream.collect(Collectors.toList());

操作 返回类型 操作类型 函数描述符
filter Stream<T> Predicate<T> T -> boolean
map Stream<R> Function T -> R
limit Stream<T>

sorted Stream<T> Comparator (T, T) -> int
distinct Stream<T>

终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚至void。

List<String> str = Lists.newArrayList("hello""world""2018");
str.stream().forEach(System.out::print);

操作 目的
forEach 消费流中的每个元素并对其应用 Lambda。这一操作返回 void
count 返回流中元素的个数。这一操作返回 long
collect 把流归约成一个集合,比如 List、Map 甚至是 Integer

接下来,我们根据不同的用处来看看如何使用这些操作。

用谓词筛选
Streams接口支持filter方法,该操作会接受一个谓词(一个返回 boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

List<Integer> nums = Lists.newArrayList(123325234);
// 筛选奇数
nums.stream().filter(n -> n % 2 == 1).forEach(System.out::print);

筛选不重复的元素
流还支持一个叫作distinct的方法,它会返回一个元素不重复(根据流所生成元素的 hashCode和equals方法实现)的流。

List<Integer> nums = Lists.newArrayList(123325234);
// 筛选不重复的奇数
nums.stream().filter(n -> n % 2 == 1).distinct().forEach(System.out::print);

截断流
流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。

List<Integer> nums = Lists.newArrayList(123325234);
// 截取前三个数字
nums.stream().limit(3).forEach(System.out::print);

跳过元素
流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一 个空流。请注意,limit(n)和skip(n)是互补的!

List<Integer> nums = Lists.newArrayList(123325234);
// 跳过前三个数字
nums.stream().skip(3).forEach(System.out::print);

映射
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映 射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一 个新版本”而不是去“修改”)。

List<Integer> nums = Lists.newArrayList(123325234);
// 将数字映射为数字的平方
nums.stream().map(n -> n * n).forEach(System.out::print);

流的扁平化
流支持flatmap方法,它让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

List<String> strs = Lists.newArrayList("hello""world");
// 过滤出所有不同的字符
strs.stream().map(str -> str.split("")).flatMap(Arrays::stream)
                .distinct().forEach(System.out::print);

检查谓词是否至少匹配一个元素
anyMatch方法可以检查“流中是否有一个元素能匹配给定的谓词”,anyMatch方法返回一个boolean,因此是一个终端操作。

List<Integer> nums = Lists.newArrayList(123325234);
// 检查是否有大于10的数字
System.out.println(nums.stream().anyMatch(num -> num > 10));// false

检查谓词是否匹配所有元素
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓。

List<Integer> nums = Lists.newArrayList(123325234);
// 检查是否所有的数字都大于10
System.out.println(nums.stream().allMatch(num -> num < 10));// true

noneMatch
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。

List<Integer> nums = Lists.newArrayList(123325234);
// 检查是否所有的数字都不大于10
System.out.println(nums.stream().noneMatch(num -> num > 10));// true

短路求值:
anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中&&和||运算符短路在流中的版本。
对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny) 不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个 短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小 的流的时候,这种操作就有用了:它们可以把无限流变成有限流。

查找任意元素
findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。它会返回一个Optional 对象,代表一个值存在或不存在(我们将在后面专门用一篇文章来讲解它),因为findAny可能什么元素都没找到。

List<Integer> nums = Lists.newArrayList(123325234);
// 查询大于1的任意元素
System.out.println(nums.stream().filter(n -> n > 1).findAny());

查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List或 排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个findFirst方法,它的工作方式类似于findany。

List<Integer> nums = Lists.newArrayList(123325234);
// 查询大于1的第一个元素
System.out.println(nums.stream().filter(n -> n > 1).findFirst());

何时使用 findFirst 和 findAny
你可能会想,为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素
在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。

元素求和
流中有一个reduce方法,它会反复结合流中的每个元素,直到流被归约成一个值。reduce方法接收两个参数,一个初始值,一个BinaryOperator 来将两个元素结合起来产生一个新值。

List<Integer> nums = Lists.newArrayList(123325234);
// 对集合中的所有数字求和
System.out.println(nums.stream().reduce(0, (a, b) -> a + b));// 25

reduce方法除了可以用来求和,还可以有其他很多用处,例如通过比较大小求最大最小值,除此之前,它还有一个重载方法,没有初始值,但是会返回一个Optional对象。

// 求最大值
nums.stream().reduce(Integer::max).ifPresent(System.out::print);// 5
// 求最小值
nums.stream().reduce(Integer::min).ifPresent(System.out::print);// 1

计算元素个数
流中有一个count方法来计算元素个数,返回一个long值。

List<Integer> nums = Lists.newArrayList(123325234);
System.out.println(nums.stream().count());// 9

数值流
前面通过reduce对元素求和时,会有一个问题,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和 LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。 此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。

Stream<Integer> stream = Stream.of(1358);
int sum = stream.mapToInt(num -> num.intValue()).sum();// 求和
OptionalDouble average = stream.mapToInt(num -> num.intValue()).average();// 求平均值
OptionalInt max = stream.mapToInt(num -> num.intValue()).max();// 求最大值

// 将Stream转化为数值流
IntStream intStream = stream.mapToInt(num -> num.intValue());
// 将数值流转化为Stream
stream = intStream.boxed();

数值范围
Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成某个范围内的数值流: range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range是不包含结束值的,而rangeClosed则包含结束值。

IntStream nums = IntStream.rangeClosed(1100);// 包含100
IntStream nums2 = IntStream.range(1100);// 不包含100

创建流
上面我们已经介绍了很多使用流的方式,大多数情形下我们都是通过集合的stream方法得到流,另外还单独介绍了通过range和rangeClosed方法创建数值流,除此之外,还有其他创建流的方式。

由值创建流
可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。

// 创建一个字符串流
Stream<String> stream = Stream.of("Hello""Java""8""2018");
// 使用empty得到一个空流
Stream<String> emptyStream = Stream.empty();

由数组创建流
可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。

// 通过数组创建流
int[] numbers = { 1246431 };
IntStream numStream = Arrays.stream(numbers);
String[] strs = { "hello""world" };
Stream<String> strStream = Arrays.stream(strs);

由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。

// 从文件创建流
try (Stream<String> lines = Files.lines(Paths.get("data.txt"),
                Charset.defaultCharset())) {
    // TODO
catch (Exception e) {
    // TODO
}

由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterateStream.generate。 这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate 2 和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说, 应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

// 通过迭代创建流
Stream<Integer> intStream = Stream.iterate(0, n -> n + 2).limit(10)

iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的 Lambda(UnaryOperator 类型)。这种iterate操作基本上是 顺序的, 因为结果取决于前一次应用。请注意,此操作将生成一个 无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是 无界的。

//通过generate创建流
Stream<Double> doubleStream = Stream.generate(Math::random).limit(5);

与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier 类型的Lambda提供新的值。

结束语

本文介绍了很多针对流的用法,下一篇文章中我还将继续介绍如何通过流来收集数据,通过这两篇文章的学习之后,你将不会再愿意回到循环的世界中。

本文案例源码:

https://github.com/lanheixingkong/HelloJava8

参考资料

  • 《Java 8实战》

PS:这篇文章已经停更了很长时间,中间也发生了很多的事情,但是自己说过的话就要努力做到,自己挖的坑跪着也要填完,刚把得。


0条留言

留言