导言:
最近在工作当中遇到了很多内容是设计到流的,发现流这个东西在工作当中还是很值得学习,所以就在《Java核心技术卷II》里面专门来总结一下流。主要涉及到的内容:
- 从迭代到流的操作
- 流的创建
- filter、map和flatMap方法
- 抽取子流和组合流
- 其他的流转换
- 简单约简
- Optional类型
- 收集结果
- 收集到映射表中
- 群组和分区
- 下游收集器
- 约简操作
- 基本类型流
- 并行流
与集合相比,流提供了一种可以让我们在更高的概念级别上指定计算任务的数据视图。通过使用流,我们可以说明我们想要完成什么任务,而不是说明如何去实现它,将操作的调度留给具体实现去解决。
举例:
计算某个属性的平均值的场景。就将这个属性所在数据源以及该属性指定好,然后用流库对"计算流的平均值"这个操作进行优化。优化:可以用多线程来计算总与个数,然后将结果合并。
流的原则— 做什么而非怎么做。
处理集合时,我们一般会将集合里的元素迭代遍历,然后再每个元素上进行某项操作。
LetMeShowU.txt
Hello guys This is LetMeShowU And I will public the code in github.
可以看到LetMeShowU.txt的文本里面有4个词的长度是大于4的
package com.example.demo;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
public class StreamDemo {
public static void main(String[] args) throws IOException {
// 迭代遍历的方式
int count = 0;
String contents = new String(Files.readAllBytes(Paths.get("src\\main\\resources\\txt\\LetMeShowU.txt")));
List<String> words = Arrays.asList(contents.split("\\PL+"));
for (String w : words) {
if (w.length() > 4) {
count++;
}
}
System.out.println("迭代遍历得到的count结果: " + count);
// 流的方式
long countInStream = words.stream().filter(w -> w.length() > 4).count();
System.out.println("流操作得到的结果: " + countInStream);
// 并行流的方式
long countInParallelStream = words.parallelStream().filter(w -> w.length() > 4).count();
System.out.println("并行流操作得到的结果: " + countInParallelStream);
}
}
执行结果:
迭代遍历得到的count结果: 4
流操作得到的结果: 4
并行流操作得到的结果: 4
可以看到,迭代遍历的方式会需要去扫描整个代码去过滤和做计数操作,而流不需要做这些操作,流的方法名就可以直接的告诉我们流里的代码是在做什么:
filter() 过滤操作
count() 计数操作
然后将代码里的stream改为parallelStream就可以让流库以并行的方式来执行过滤和技术。
并行流:
并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据库的流。
本文章后面会对并行流有更详细的讲解。
表面上,流和集合看起来很类似,都可以让我们达到转换和获取数据的目的,但是流和集合之间存在着明显的差异:
第一个差异: 流并不存储元素
在集合当中,元素可能存储在底层的集合中,或者是按需生成的。而流,不存储数据源中的元素。
第二个差异:流的操作不会修改其数据源
例如,filter()方法不会从流中移除元素,而是会生成一个新的流,新的流中不包含被filter()方法里面过滤掉的元素。
第三个差异:流的操作尽可能惰性执行
这个差异意味着,流只有在需要结果的时候,才会执行流里的操作。举例:
如果我们只想查找前5个长单词而不是所有的长单词,那么filter方法就会在匹配到第5个单词后停止过滤。因此,我们甚至可以操作无限流。
如Demo所展示,stream()和parallelStream()方法会产生一个用于words列表的流,而filter()方法会返回另外一个流,filter()这个方法返回的流中只包含长度大于12的单词。而count()方法会将这个流化简为一个结果。
Demo里面的工作流是操作流时的典型流程,我们建立了一个包含三个阶段的操作管道:
第一阶段:创建一个流
集合通过stream()和parallelStream()创建的流。
第二阶段:指定将初始流转换成其他流的中间操作,中间操作可能包含多个步骤
filter()或者其他的中间操作,例如排序操作sort(),中间操作可以包含多个步骤。
第三阶段:应用终止操作,从而产生结果
终止操作会强制执行之前的惰性操作,从此以后,这个流就再也不能用了。
API: java.util.stream.Stream
Stream<T> filter(Predicate<? super T> p)
产生一个流,其中包含当前流中满足p的所有元素
long count()
产生当前流中元素的数量。这是一个终止操作
API: java.util.Collection
default Stream<E> stream()
产生当前集合中所有元素的顺序流
default Stream<E> parallelStream()
产生当前集合中所有元素的并行流
Collection接口的stream方法可以将任何集合转换成一个流。
使用静态方法 Stream.of方法
// split()会返回一个String[] array
Stream<String> words = Stream.of(contents.split("\\PL+"));
静态方法 Stream.of方法具有可变长参数,所以可以通过这个可变长参数构建具有任意数量引元的流:
Stream<String> cxk = Stream.of("鸡你太美", "你干嘛?", "诞生于1996");
创建不包含任何元素的流,可以使用静态的Stream.empty()方法
Stream<String> emptyStream = Stream.empty();
Stream提供了两种方式用于创建无限流的静态方法
generate()方法会接受一个不包含任何引元的函数(技术层面看,是一个Supplier接口的对象)。
无论何时,只需要一个流类型的值,该函数就会被调用产生一个这样的值。下面是获取一个常量值的流
Stream(String) lilHater = Stream.generate(() -> "蔡徐坤"):
获取随机数的无限流
Stream<Double> randoms = Stream.generate(Math::random);
如果要产生像0 1 2 3 4…这样的序列,可以使用Stream.interate()方法。该方法有两个参数,一个叫做种子值以及一个函数(技术层面看,是一个UnaryOperation),并且会反复地将该函数应用的之前的结果上,例如:
// Stream.iterate()方法获取无限流
Stream<BigInteger> integerStream = Stream.iterate(BigInteger.ZERO, number -> number.add(BigInteger.ONE));
该序列中,BigInteger.ZERO是种子,即0,第二个原始是f(seed),即1, 则第三个元素是f(f(seed)),即2,以此类推。
如果要获取一个有限序列,则需要加一个谓词来描述迭代应该如何结束。
// 使用谓词获得有限序列(前提是先得到一个无线序列)
int limit = 3;// 谓词
Stream<Integer> integerStream = Stream.iterate(0, n -> n + 1);
integerStream.limit(limit).forEach(System.out::println);
这里打印的结果为:
0
1
2
Stream.ofNullable()方法会用一个对象来创建一个非常短的流。如果对象为null,那么这个流的长度为0;否则,这个流的长度为1,即只包含该对象。这个方法结合flatMap时最有用。
流的转换会产生一个新的流,新的流的元素派生自另外一个流中的元素。
filter转换会产生一个新的流,新的流的元素与某种条件匹配。
// 案列:将一个字符串流转换为只包含长单词的另一个流
// 案列: 将一个字符串流转换为只包含长单词的另一个流
List celebrityList = Arrays.asList("caixukun", "wangziyi", "chenlinong", "fanchengcheng", "ikun");
long longNameCelebrity = celebrityList.stream().filter(celebrity -> celebrity.length() > 8).count();
System.out.println("名字长度大于8的有" + longNameCelebrity + "个");
结果是:名字长度大于8的有2个
通常,我们想要按照某种方式来转换流中的值,此时,可以使用map方法并传递执行该转换的函数。例如可以将上面的celebrityStream里面的元素都转换成大写:
celebrityList.stream().map(String::toUpperCase).forEach(c -> System.out.println(c));
结果是:
CAIXUKUN
WANGZIYI
CHENLINONG
FANCHENGCHENG
IKUN
在使用map时,会有一个函数应用到每个元素上,并且其结果包含了应用该函数后所产生的所有结果的流。
假设我们现在定义一个codePoints方法,方法的返回不是一个值,而是一个流。
private static Stream codePoints(String s) {
ArrayList result = new ArrayList<>();
int i = 0;
while (i < s.length()) {
int j = s.offsetByCodePoints(i, 1);
result.add(s.substring(i, j));
i = j;
}
return result.stream();
}
那么,如果调用codePoints方法,比如
celebrityList.stream().map(String::toUpperCase).forEach(c -> System.out.println(c));
codePoints("LetMeShowU").forEach(s -> System.out.println(s));
那么结果会是:
L
e
t
M
e
S
h
o
w
U
如果将codePoints方法在中间操作中运用到,那会得到一个包含流的流。
Stream> streamInStream = celebrityStream.stream().map(c -> codePoints(c));
那么现在这个streamInStream这个流里面就像是[[“c”, “a”, “i”, “x”, “u”, “k”, “u”, “n”], [“w”, “a”, “n”, “g”, “z”, “i”, “y”, “i”], […]];
如果我们要将其摊平成为单个流,那就使用flatMap方法.
Stream singleStream = celebrityList.stream().flatMap(c -> codePoints(c));
那么现在得到的会是[“c”, “a”, “i”, “x”, “u”, “k”, “u”, “n”, “w”, “a”, “n”, “g”, “z”, “i”, “y”, “i”,…]
#####3.3 flatMap
flatMap方法是计算机科学中的一种通用概念。
假设我们有一个泛型G(比如Stream),以及可以将某种类型T转换成G(U)的函数functionA;
又假设我们还有一个可以将类型U转换成G(V)的函数functionB. 我们可以通过flatMap来组合他们,即先用functionA再用functionB. 这是单子论的关键概念。
stream.limit(n)会返回一个新的流,它在n个元素之后结束(如果原来的流比n短,那么就会在该流结束时结束);
limit()一般用来裁剪无限流的尺寸。
案例:产生一个包含100个随机数的流。
Stream hundredRandomNumbers = Stream.generate(Math::random).limit(100);
stream.skip()与limit()相反,skip()会丢弃前n个元素。
stream.takeWhile(predicate) 调用会在谓语为真时获取流中所有的元素,然后停止。
dropWhile与takeWhile相反,条件为真时,丢弃元素,并产生一个由第一个使该条件为假的字符开始的所有元素构成流
Stream类的静态方法concat可以将两个流连起来
Stream helloWorldStream = Stream.concat(codePoints("hello"), codePoints("world"));
那么此时流中的元素会是: h e l l o w o r l d
在使用concat时,第一个流不应该是无限的,不然第二个流永远不会有机会处理。
其他流转换
distinct会返回一个流,流中的元素是从原有的流中产生的,即原来的元素按照同样的顺序剔除重复元素后产生的新流。
注意,不是相邻的相同的元素,也会被剔除。
对于流里的元素的排序,有多种sorted方法的变体可用。
一种用于操作Comparable元素的流,另外一种可以接受一个Comparator。
celebrityList.stream().sorted(Comparator.comparing(String::length).reversed()).forEach(c -> System.out.println(c));
打印结果为:
fanchengcheng
chenlinong
caixukun
wangziyi
ikun
当我们除了排序之外还有其他操作,那使用流是比直接使用集合更加方便的。
peek方法会产生另外一个流,新流与旧流的元素相同,但是每次获取一个元素时,都会调用一个函数。
Object[] powers = Stream.iterate(1.0, p -> p * 2).peek(e -> System.out.println("Fetching" + e)).limit(5)
.toArray();
代码里面先得到从1开始,接下来的每个元素在前一个元素的基础上乘以2的元素,共计5个。但是又通过peek里面的打印操作,将这5个元素打印到控制台中。
所以控制台会显示:
Fetching1.0
Fetching2.0
Fetching4.0
Fetching8.0
Fetching16.0
约简是一种终结操作(terminal operation),它会将流约简为可以在程序中使用的非流值。
返回流中的元素的数量
min和max返回最大和最小值。但是返回的值是一个Optional类型,Optional属于一种容器,这个容器可以表示有值或者也可以表示没有任何值。我们经常因为返回了null而报空指针异常,而Optional是一种表示缺少返回值的更好的方式。
返回费控集合中的第一个值
常用场景一般会和filter来用,比如找到第一个以字母Q开头的单词,当然,前提是存在这样的单词:
Optional firstGuyNameQ = celebrityList.stream().filter(c -> c.startsWith("Q")).findFirst();
如果不强调第一个匹配,而是使用任意匹配都可以,那么久可以使用findAny方法
findAny方法在并行处理流时很有效,因为流可以报告任何它找到匹配而不是被限制成必须报告第一个。
Optional guysNameQ = celebrityList.stream().filter(c -> c.startsWith("Q")).findAny();
是否存在匹配,可以使用anyMatch。
该方法会接受一个断言引元,因此不需要filter。
boolean isGuyNameC = celebrityList.stream().anyMatch(c -> c.startsWith("c"));
分别在所有元素和没有任何元素匹配谓词的情况下返回true
这些可以通过并行运行而优化。