这一篇带你系统学习一下Stream

这一篇带你系统学习一下Stream

Java8中最大的两个亮点,一个是Lambda表达式,另一个就是Stream。新特性的加入,一定是为了某种需求,那么Stream是什么,它能帮助我们做什么?首先看下面这个例子:

有这样一份数据,一组考卷List,每个Paper有三个属性分别是学生名字studentName、课程名称className和分数score。现在我们需要从中找出语文不及格(分数低于60)的学生名字,并且按分数从高到低排序。在不使用Java8新特性之前,相信大部分人都是像下面这样写的:
public static List getFailedPaperStudentNamesByJava7(List papers) {
// 筛选出不及格的考卷
List failedPapers = new ArrayList<>();
for (Paper paper : papers) {
if (paper.getClassName().equals(“语文”)
&& paper.getScore() < 60) {
failedPapers.add(paper);
}
}
// 按分数从高到低排序
Collections.sort(failedPapers, new Comparator() {
@Override
public int compare(Paper o1, Paper o2) {
return o2.getScore() - o1.getScore();
}
});
// 记下不及格的学生名字
List failedPaperStudentNames = new ArrayList<>();
for (Paper failedPaper : failedPapers) {
failedPaperStudentNames.add(failedPaper.getStudentName());
}
return failedPaperStudentNames;
}

下面是用Java8的Lambda表达式+Stream改写的版本:
public static List getFailedPaperStudentNamesByJava8(List papers) {
return papers.stream()
.filter(p -> p.getClassName().equals(“语文”)
&& p.getScore() < 60)
.sorted((p1, p2) -> p2.getScore() - p1.getScore())
.map(Paper::getStudentName)
.collect(Collectors.toList());
}

可直观的看出,代码量少了,只用了一行代码把所有操作链接起来了。我们再细看下,从这方法的名字上去理解,首先通过stream从List获得Stream,然后可以使用流式操作处理数据,先是filter筛选出语文课且不及格的考卷,接着sorted对分数排序,再是map获得每个Paper中的学生名字,最后collect把所有的名字收集成一个List。可看出,从语义上的理解也更为直观了,在筛选语文课不及格的试卷时,我们不是使用命令式写法(遍历,然后判断,再放到一个新的List里),而是类似SQL中的where条件,通过声明式写法直接给出数据需要符合的条件,便能得到需要的数据。

我们说了很久的“Stream”,到底什么是“Stream”,笔者从Stream API这个角度谈自己的理解:

Stream是Java提供的一个接口,该接口允许以声明式的写法处理数据,可以把操作链接起来,形成数据处理流水线,还能将数据处理任务并行化。

声明式和链接操作,前面的例子已经能看出。那什么是并行化,例如统计学生名字,我们可以将该任务划分成多个,交给多个CPU分别计算,最后再汇总结果,这一系列复杂操作,可交给Stream完成,如下:
public static List getFailedPaperStudentNamesByJava8(List papers) {
return papers.parallelStream()
.filter(p -> p.getClassName().equals(“语文”)
&& p.getScore() < 60)
.sorted((p1, p2) -> p2.getScore() - p1.getScore())
.map(Paper::getStudentName)
.collect(Collectors.toList());
}

只是将stream()方法改为parallelStream()方法就能将该任务并行化,是不是十分简单。

通过以上介绍,想必已对Stream有了初步认识,下面开始系统学习Stream。

  1. 流和集合

首先我们还是要弄清楚流和集合在概念上的区别。集合,例如List、Set、Tree等,是一种存储数据的数据结构。关于数据,是已经存在了的,我们只是通过一种数据结构将数据组织起来,便于某种方式读取或保持某种结构。流不同于集合的地方在于数据并非在使用前全部获得,而是在使用过程中按需获得。例如文件流,我们可以通过readline将文件一行一行的读取。还有视频流,我们可以边看边下载,不用等将所有数据下载完毕才能观看。

所以虽然我们都能从集合、流中获取数据,但数据产生的时间是有区别的,集合的数据是预先产生的,而流则是根据需要实时产生的。两者的特性也导致用途上的差异,集合侧重存储,流侧重计算。因此我们常听到的流式计算的叫法。

下面说下流和集合在使用上的区别。集合,可以随时取用,但流在创建后只能被使用一次,若重复消费,则会报错。
List list = Arrays.asList(“A”, “B”, “C”);
Stream stream = list.stream();
stream.forEach(System.out::print);
stream.forEach(System.out::print);
// ABC
// java.lang.IllegalStateException: stream has already been operated upon or closed

另外,集合在遍历时,就像我们前面描述的例子,只能通过程序员编写 for-each 这种显示的代码去迭代,这被称作外部迭代。而流在遍历时,例如map会对流中的每个元素进行处理,所以我们不需要写具体的迭代代码,而是交由Java内部完成,这被称作内部迭代。内部迭代的好处在于,它是一个黑盒。你需要的是迭代,那Java只要完成你的目标就行了。而至于如何迭代,则交由Java来完成,这就为优化提供了可能,优化的方向有两点,一是更优化的顺序来处理,二是将操作并行化,例如我们在学生的示例中,只是将stream改成parallelStream(),后续其他的map操作可以根据判断是并行流从而将任务并行化,而我们不用修改map操作。

下面我们来学习如何使用流。

  1. 创建流

在对流进行操作之前,我们首先需要获得一个Stream对象,创建流有以下几种方式。

2.1 集合

Collection的默认方法stream(),可以由集合类创建流。
List list = Arrays.asList(“A”, “B”, “C”);
Stream stream = list.stream();

2.2 值

Stream的静态方法of(T… values),通过显示值创建流,可接受任意数量的参数。
Stream stream = Stream.of(“A”,“B”,“C”);

2.3 数组

Arrays的静态方法stream(T[] array)从数组创建流,接受一个数组参数。
String[] ss = new String[]{“A”, “B”, “C”};
Stream stream = Arrays.stream(ss);

2.4 文件

NIO中有较多静态方法创建流,例如Files的静态方法lines(Path path)从返回指定文件中的各行构成的字符串流。
try (Stream stream = Files.lines(Paths.get(“data.txt”))) {
stream.forEach(System.out::print);
} catch (IOException e) {
}

2.5 函数

Stream的静态iterate和generate可根据函数计算创建无限流。首先看下iterate,通常用于依次生成一系列值,其声明如下:
Stream iterate(final T seed, final UnaryOperator f)

seed为初始值,UnaryOperator是Function<T,R>的子类,区别在于规定输入、输出都是T类型,下面看个示例:
Stream.iterate(0, n -> n + 1)
.forEach(System.out::println);

该示例会根据初始值,然后通过函数计算依次得到下一个值,0、1、2…你可以试着运行下,发现根本停不下来,这就是刚刚说到的无限流。我们可以通过limit(n)来对无限流做限制。
Stream.iterate(0, n -> n + 1)
.limit(10)
.forEach(System.out::println);

接着是generate,不同于iterate依次根据上次计算的结果生成, 而是通过一个Supplier实例提供新的值,其声明如下:
Stream generate(Supplier s)

例如,我们生成10个随机数:
Stream.generate(Math::random)
.limit(10)
.forEach(System.out::println);

2.6 数值流

你可能已经注意到上述介绍的Stream使用了泛型,所以可以适用于任意引用类型,而对于原始类型则只能使用其包装类,例如:
Stream.of(1, 2, 3)
.forEach(n -> {
System.out.println(n.getClass()); // Integer
int x = n * 2; // 需要拆箱
});

Stream在处理原始类型上会由于装箱拆箱造成较大的性能损耗,所以Java8提供了三种特殊的流接口IntStream、DoubleStream、LongStream,将流中的元素特化为int、double和long。
IntStream.of(1, 2, 3)
.forEach(n -> {
int x = n * 2; // n为int
});

除了使用of创建流,还可以将普通流转成数值流,mapToInt、mapToDouble和mapToLong。
Stream.of(1, 2, 3)
.mapToInt(Integer::intValue)
.forEach(n -> {
int x = n * 2; // n为int
});

数值流也能转成普通流,boxed装箱。
IntStream.of(1, 2, 3)
.boxed()
.forEach(n -> {
int x = n * 2; // n为Integer
});

  1. 流的操作

流创建好了,下面学习对流进行操作。流的操作分为两种:

中间操作:返回一个Stream对象,可以将一系列中间操作构成一条流的流水线(类似构造器模式)

终端操作:执行流水线,返回不是流的结果(也可是void)

public static List getFailedPaperStudentNamesByJava8(List papers) {
return papers.parallelStream()
.filter(p -> p.getClassName().equals(“语文”) // Stream
&& p.getScore() < 60)
.sorted((p1, p2) -> p2.getScore() - p1.getScore()) // Stream
.map(Paper::getStudentName) // Stream
.collect(Collectors.toList()); // List
}

这里的filter、sorted、map就是中间操作,collect为终端操作。终端操作用于执行流水线是什么意思?意思是如果没有终端操作,将不会执行前面链接的中间操作。例如:
List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.map(s -> {
System.out.print(s);
return s;
});
// 无输出

无终端操作的情况下,中间操作map里的代码块将不会执行,不会有输出。而我们在此基础上加上一个终端操作forEach,便能触发流水线的执行。
List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.map(s -> {
System.out.print(s);
return s;
})
.forEach(s -> {});
// ABC

下面来看下常用的流操作。

3.1 筛选

(1)filter
Stream filter(Predicate<? super T> predicate)

过滤

接受一个谓词Predicate(T -> boolean)

返回一个包含所有符合谓词的元素的流。

List list = Arrays.asList(“AA”, “AB”, “BC”);
list.stream()
.filter(s -> s.startsWith(“A”))
.forEach(System.out::println);
// AA
// AB

(2)distinct
Stream distinct()

去重

根据流中元素的hashCode和equals方法比较元素

返回一个元素各异的流

List list = Arrays.asList(“A”, “A”, “B”);
list.stream()
.distinct()
.forEach(System.out::print);
// AB

3.2 切片

(1)limit
Stream limit(long maxSize);

截断

接受一个长度

返回一个不超过给定长度的流

List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.limit(2)
.forEach(System.out::print);
// AB

(2)skip
Stream skip(long n)

跳过元素

指定跳过前n个元素

如果元素不足n个,返回一个空流

List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.skip(2)
.forEach(System.out::print);
// C

3.3 映射

(1)map
Stream map(Function<? super T, ? extends R> mapper)

对每个元素应用函数

接受一个函数(T -> R)

将每一个元素映射成一个新的元素

List papers = Arrays.asList(
new Paper(“小明”, “语文”, 40),
new Paper(“小红”, “语文”, 80),
new Paper(“小蓝”, “语文”, 50)
);
papers.stream()
.map(Paper::getStudentName)
.forEach(System.out::println);
// 小明
// 小红
// 小蓝

(2)flatMap
Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

流的扁平化,什么意思,先看下面这个例子:
List list = Arrays.asList(“ABC”, “DEF”, “GHI”);
list.stream()
.map(s -> s.split("")) // Stream<String[]>
.forEach(System.out::println);
// [Ljava.lang.String;@2f4d3709
// [Ljava.lang.String;@4e50df2e
// [Ljava.lang.String;@1d81eb93

我们想将字符串“ABC”,“DEF”,“GHI”三个字符中的每个字符组合成一个流然后打印出来,但是上述的写法,通过split函数拆分了String[]数组,流中的元素也被映射成了数组,例如String[]{“A”, “B”, “C”},所以forEach打印得到的结果是数组地址。

我们如何才能把数组中的元素组合在一起,得到"A", “B”, “C”, “D”…的一个流呢。这就需要扁平化的处理。flatMap接受一个函数(T -> Stream),把流中每个元素映射为一个流,然后再把所有的流组合成一个最终的流。例如这里的元素是String[],那我们就把数组映射成流Arrays::stream,这样就能把每个数组里的元素连接在一起了。
List list = Arrays.asList(“ABC”, “DEF”, “GHI”);
list.stream()
.map(s -> s.split("")) // Stream<String[]>
.flatMap(Arrays::stream) // Steam
.forEach(System.out::println);

3.4 匹配

(1)anyMatch
boolean anyMatch(Predicate<? super T> predicate)

至少匹配一个元素

接受一个谓词(T -> boolean)

如果有一个元素匹配,返回true,否则返回false

List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.map(s -> {
System.out.print(s);
return s;
})
.anyMatch(s -> s.startsWith(“B”)); // true
// AB

当遇到B时,匹配到了,便会直接返回,不会再迭代后续元素,这是一种短路操作。

(2)allMatch
boolean allMatch<Predicate<? super T> predicate>

匹配所有元素

接受一个谓词(T -> boolean)

所有所有元素匹配,返回true,否则返回false

当有一个元素不匹配,就会短路返回

List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.map(s -> {
System.out.println(s);
return s;
})
.allMatch(s -> s.startsWith(“B”)); // false
// A

(3)nonMatch
boolean nonMatch(Predicate<? super T> predicate)

所有元素不匹配

接受一个谓词(T -> boolean)

所有元素匹配,返回true,否则返回false

当有一个元素匹配,就会短路返回

List list = Arrays.asList(“A”, “B”, “C”);
list.stream()
.map(s -> {
System.out.println(s);
return s;
})
.noneMatch(s -> s.startsWith(“B”)); // false
// A

3.5 查找

(1)findAny
Optional findAny()

返回当前流中的任意元素,用Optional封装元素,迫使显示检查元素是否存在。

(2)findFirst
Optional findFirst()

返回当前流中的第一个元素

对比:

两者都是返回一个元素,如果不关心返回的元素是哪个,优先使用findAny,因为这样在并行上的限制更少,可优化的空间更大。

3.6 归约

reduce
// 有初始值
T reduce(T identity, BinaryOperator accumulator)
// 无初始值
Optional reduce(BinaryOperator accumulator)

通过接收一个BinaryOperator(T, T) -> T,将两个元素结合产生一个新值。reduce将一直执行该操作直到最后流中只剩一个元素返回。
int[] nums = new int[]{1, 2, 3, 4, 5};
int sum = IntStream.of(nums).reduce(0, Integer::sum); // 15
int max = IntStream.of(nums).reduce(Integer::max).orElse(-1); // 5
int min = IntStream.of(nums).reduce(Integer::min).orElse(-1); // 1

3.7 数值流的特殊操作

在2.6节中我们说过针对原始类型有特殊的原始类型流,由于都是数值,所以也设计了些针对数值的方法。
int[] nums = new int[]{1, 2, 3, 4, 5};
int sum = IntStream.of(nums).sum(); // 15,等同reduce(0, Integer::sum)
int max = IntStream.of(nums).max().orElse(-1); // 5,等同reduce(Integer::max).orElse(-1)
int min = IntStream.of(nums).min().orElse(-1); // 1,等同reduce(Integer::min).orElse(-1)
double avg = IntStream.of(nums).average().orElse(-1); //3.0

3.8 收集

collect在前面的示例中已经见过了,可以将流中的元素进行汇总。
<R, A> R collect(Collector<? super T, A, R> collector);

接收一个Collector收集器。在Collectors中已经内置了一些常用的收集器。

toList():将元素收集成一个List。

toSet():将元素收集成一个Set。

counting():统计元素数量。

maxBy(Comparator<? super T> comparator):获取元素中的最大值。

minBy(Comparator<? super T> comparator):获取元素中的最小值。

summingInt(ToIntFunction<? super T> mapper):将元素映射成一个int值,然后求和,类似的还有double和long。

averagingInt(ToIntFunction<? super T> mapper):将元素映射成一个int值,然后求平均。

summarizingInt(ToIntFunction<? super T> mapper):将元素映射成一个int值,然后得到一个IntSummaryStatistics对象,包含了统计数、总和、最大值、最小值和平均值。

joining():把元素toSting()的结果连接成一个字符串,还有一个重载版本,接收一个分隔符参数。

groupingBy(Function<? super T, ? extends K> classifier):接收一个Function,返回一个Map<K, List>。通过Function的返回值作为Key,然后将具有相同Key的元素,组合成List。

下面看示例:
List papers = Arrays.asList(
new Paper(“小明”, “语文”, 40),
new Paper(“小明”, “数学”, 80),
new Paper(“小红”, “语文”, 80),
new Paper(“小红”, “数学”, 80),
new Paper(“小蓝”, “语文”, 50),
new Paper(“小蓝”, “数学”, 60)
);
// 所有语文卷子
List chinesePapers = papers.stream()
.filter(p -> p.getClassName().equals(“语文”))
.collect(toList());
// 所有学科
Set classNames = papers.stream()
.map(Paper::getClassName)
.collect(toSet());
// 最高分的卷子,最低分改成minBy就行
Paper maxScorePaper = papers.stream()
.collect(maxBy((p1, p2) -> p1.getScore() - p2.getScore())).get();
// 总分数
int sumScore = papers.stream()
.collect(summingInt(Paper::getScore));
// 平均分
double avgScore = papers.stream()
.collect(averagingInt(Paper::getScore));
// 统计数、总和、最大值、最小值和平均值
IntSummaryStatistics summaryStatistics = papers.stream()
.collect(summarizingInt(Paper::getScore));
long count = summaryStatistics.getCount();
long sum = summaryStatistics.getSum();
int max = summaryStatistics.getMax();
int min = summaryStatistics.getMin();
double avg = summaryStatistics.getAverage();
// 学生名字连接在一起
String studentNameStr = papers.stream()
.map(Paper::getStudentName)
.distinct()
.collect(joining(","));
// 按学科将卷子分组
Map<String, List> groupPapers = papers.stream()
.collect(groupingBy(Paper::getClassName));

以上介绍的是常用的Collector,我们还可以根据需要自定义Collector,本文就不叙述了。

  1. 总结

本文列举了在日常开发中较为常用的流操作,但是还有未涉及之处,感兴趣的读者可以直接看Stream的API。本文也没有讲述并行流,虽然用法简单,但是能否真正提高效率,还是要看具体情况,还缺乏经验就不叙述了。先掌握基本的流式操作吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值