Stream One
// 从一个球队集合中过滤出积分大于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(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 筛选奇数
nums.stream().filter(n -> n % 2 == 1).forEach(System.out::print);
筛选不重复的元素
流还支持一个叫作distinct
的方法,它会返回一个元素不重复(根据流所生成元素的 hashCode和equals方法实现)的流。
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 筛选不重复的奇数
nums.stream().filter(n -> n % 2 == 1).distinct().forEach(System.out::print);
截断流
流支持limit(n)
方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 截取前三个数字
nums.stream().limit(3).forEach(System.out::print);
跳过元素
流还支持skip(n)
方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一 个空流。请注意,limit(n)和skip(n)是互补的!
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 跳过前三个数字
nums.stream().skip(3).forEach(System.out::print);
映射
流支持map
方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映 射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一 个新版本”而不是去“修改”)。
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 将数字映射为数字的平方
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(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 检查是否有大于10的数字
System.out.println(nums.stream().anyMatch(num -> num > 10));// false
检查谓词是否匹配所有元素allMatch
方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓。
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 检查是否所有的数字都大于10
System.out.println(nums.stream().allMatch(num -> num < 10));// true
noneMatch
和allMatch相对的是noneMatch
。它可以确保流中没有任何元素与给定的谓词匹配。
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 检查是否所有的数字都不大于10
System.out.println(nums.stream().noneMatch(num -> num > 10));// true
短路求值:
anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路
,这就是大家熟悉的Java中&&和||运算符短路在流中的版本。
对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny) 不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个 短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小 的流的时候,这种操作就有用了:它们可以把无限流变成有限流。
查找任意元素findAny
方法将返回当前流中的任意元素。它可以与其他流操作结合使用。它会返回一个Optional
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 查询大于1的任意元素
System.out.println(nums.stream().filter(n -> n > 1).findAny());
查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List或 排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个findFirst
方法,它的工作方式类似于findany。
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 查询大于1的第一个元素
System.out.println(nums.stream().filter(n -> n > 1).findFirst());
何时使用 findFirst 和 findAny
你可能会想,为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素
在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
元素求和
流中有一个reduce
方法,它会反复结合流中的每个元素,直到流被归约成一个值。reduce方法接收两个参数,一个初始值,一个BinaryOperator
List<Integer> nums = Lists.newArrayList(1, 2, 3, 3, 2, 5, 2, 3, 4);
// 对集合中的所有数字求和
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(1, 2, 3, 3, 2, 5, 2, 3, 4);
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(1, 3, 5, 8);
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(1, 100);// 包含100
IntStream nums2 = IntStream.range(1, 100);// 不包含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 = { 1, 2, 4, 6, 4, 3, 1 };
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.iterate
和Stream.generate
。 这两个操作可以创建所谓的无限流
:不像从固定集合创建的流那样有固定大小的流。由iterate 2 和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说, 应该使用limit(n)
来对这种流加以限制,以避免打印无穷多个值。
// 通过迭代创建流
Stream<Integer> intStream = Stream.iterate(0, n -> n + 2).limit(10)
iterate
方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的 Lambda(UnaryOperator顺序
的, 因为结果取决于前一次应用。请注意,此操作将生成一个
无限流
——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是
无界
的。
//通过generate创建流
Stream<Double> doubleStream = Stream.generate(Math::random).limit(5);
与iterate方法类似,generate
方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier
结束语
本文介绍了很多针对流的用法,下一篇文章中我还将继续介绍如何通过流来收集数据,通过这两篇文章的学习之后,你将不会再愿意回到循环的世界中。
本文案例源码:
https://github.com/lanheixingkong/HelloJava8
参考资料
- 《Java 8实战》
PS:这篇文章已经停更了很长时间,中间也发生了很多的事情,但是自己说过的话就要努力做到,自己挖的坑跪着也要填完,刚把得。
0条留言