Java SE 8 的流库
流提供了一种让我们可以在比集合更高的概念级别上指定计算的数据视图。通过使用流,我们可以说明想要完成什么任务,而不是说明如何去实现它。我们将操作的调度留给具体实现去解决。例如,假设我们想要计算某个属性的平均值,那么我们就可以指定数据源和该属性,然后,流库就可以对计算进行优化,例如,使用多线程来计算总和与个数,并将结果合并。
Java 的流库,它是在 Java SE 8 中引入的,用来以 “做什么而非怎么做” 的方式处理集合。
从迭代到流的操作
在处理集合时,我们通常会迭代遍历它的元素,并在每个元素上执行某项操作。例如,假设我们想要对某本书中的所有长单词进行计数。
首先,将所有单词放到一个列表中
String contents = new String(Files.readAllBytes(
Paths.get("../gutenberg/alice30.txt")), StandardCharsets.UTF_8);
List<String> words = Arrays.asList(contents.split("\\PL+"));
传统方法进行迭代:
long count = 0;
for (String w : words) {
if (w.length() > 12) {
count++;
}
}
使用流进行相同的操作:
count = words.stream()
.filter(w -> w.length() > 12)
.count();
流的版本比循环版本要更易于阅读,因为我们不必扫描整个代码去查找过滤和计数操作,方法名就可以直接告诉我们其代码意欲何为。而且,循环需要非常详细地指定操作的顺序,而流却能够以其想要的任何方式来调度这些操作,只要结果是正确的即可。仅将 stream
修改为 parallelStream
就可以让流库以并行方式来执行过滤和计数
count = words.parallelStream()
.filter(w -> w.length() > 12)
.count();
流遵循了 【做什么而非怎么做】的原则。
流表面上看起来和集合很类似,都可以让我们转换和获取数据。但是,它们之间存在着显著的差异:
- 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的
- 流的操作不会修改其数据源。例如,
filter
方法不会从新的流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素 - 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如,如果我们只想查找前 5 个长单词而不是所有长单词,那么
filter
方法就会在匹配到第 5 个单词后停止过滤。因此,我们甚至可以操作无限流。
让我们再来看看这个示例。 stream
和 parallelStream
方法会产生一个用于 words
列表的 stream 。 filter
方法会返回另一个流,其中只包含长度大于 12 的单词。 count
方法会将这个流化简为一个结果。
这个工作流是操作流时的典型流程。我们建立了一个包含三个阶段的操作管道
- 创建 一个流
- 指定将初始流 转换 为其他流的中间操作,可能包含多个步骤
- 应用 终止 操作,从而产生结果。这个操作会强制执行之前的惰性操作。从此之后,这个流就再也不能用了
上面的示例,流是用 stream
和 parallelStream
方法创建, filter
方法转换,count
方法终止。
| 方法声明 | 描述 |
|
| 产生一个流,其中包含当前流中满足 |
|
| 产生当前流中元素的数量。这是一个终止操作 |
| 方法声明 | 描述 |
|
| 产生当前集合中所有元素的顺序流或并行流 |
流的创建
可以用 Collection
接口的 stream
方法将任何集合转换为一个流。如果你有一个数组,那么可以使用静态的 Stream.Of
方法:
String[] strArr = contents.split("\\PL+");
Stream<String> words = Stream.of(strArr);
of
方法具有可变长参数,因此我们可以构建具有任意数量引元的流:
Stream<String> song = Stream.of("gently", "down", "the", "stream");
使用 Arrays.stream(array, from, to)
可以从数组中位于 from
(包括) 和 to
(不包括) 的元素中创建一个流为了创建不包含任何元素的流,可以使用静态的 Stream.empty
方法:
Stream<String> stream = Arrays.stream(strArr, 0, 5);
为了创建不包含任何元素的流,可以使用静态的 Stream.empty
方法:
Stream<String> silence = Stream.empty();
Stream
接口有两个用于创建无限流的静态方法。 generate
方法会接受一个不包含任何引元的函数(或者从技术上讲,是一个 Supplier<T>
接口的对象)。无论何时,只要需要一个流类型的值,该函数就会被调用以产生一个这样的值。我们可以像下面这样获得一个常量值的流:
Stream<String> echos = Stream.generate(() -> "Echo");
或者获取一个随机数的流:
Stream<Double> randoms = Stream.generate(Math::random);
为了产生无限序列,例如 0 1 2 3 …,可以使用 iterate
方法。它会接受一个 “种子” 值,以及一个函数(从技术上讲,是 UnaryOperation<T>
),并且会反复地将该函数应用到之前的结果上
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
该序列中的第一个元素是种子 Biglnteger.ZERO
,第二个元素是 f(seed)
,即 1 (作为大整数),下一个元素是 f(f(seed)) ,即 2 ,后续以此类推
Java API 中有大量方法都可以产生流 例如, Pattern
类有一个 splitAsStream
方法,它会按照某个正则表达式来分割一个 CharSequence
对象 可以使用下面的语句来将一个字符串分割为一个个的单词:
Stream<String> wordsAnotherWay = Pattern.compile("\\PL+").splitAsStream(contents);
静态的 Files.lines
方法会返回一个包含了文件中所有行的 Stream
:
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
show("lines", lines);
}
| 方法声明 | 描述 |
|
| 产生一个元素为给定值的流 |
|
| 产生一个不包含任何元素的流 |
|
| 产生一个无限流,它的值是通过反复调用函数 |
|
| 产生一个无限流,它的元素包含种子、在种子上调用 |
| 方法声明 | 描述 |
|
| 产生一个流,它的元素是由数组中指定范围内的元素构成的 |
| 方法声明 | 描述 |
|
| 产生一个流,它的元素是输入中由该模式界定的部分 |
| 方法声明 | 描述 |
|
| 产生一个流,它的元素是指定文件中的行,该文件的字符集为 UTF-8 ,或者为指定的字符集 |
| 方法声明 | 描述 |
|
| 提供一个值 |
filter
、map
、flatMap
方法
流的转换会产生一个新的流,它的元素派生自另一个流中的元素
filter
转换会产生一个流,它的元素与某种条件相匹配。filter
的引元是 Predicate<T>
,即从 T
到 boolean
的函数
count = words.stream().filter(w -> w.length() > 12).count();
通常,我们想要按照某种方式来转换流中的值,此时,可以使用 map
方法并传递执行该转换的函数
Stream<String> upperCaseWords = words.stream().map(String::toUpperCase);
在使用 map
时,会有一个函数应用到每个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流
为了将流摊平,可以使用 flatMap
而不是 map
public class FlatMapTest {
public static void main(String[] args) {
Stream<String> words1 = Stream.of("a", "b", "c");
Stream<Stream<String>> streamStream = words1.map(w -> toStream(w));
streamStream.forEach(System.out::println);
System.out.println("================");
Stream<String> words2 = Stream.of("a", "b", "c");
Stream<String> stream = words2.flatMap(w -> toStream(w));
stream.forEach(System.out::println);
}
public static Stream<String> toStream(String str) {
return Stream.of(str + "1", str + "2", str + "3");
}
}
输出为:
java.util.stream.ReferencePipeline$Head@6d03e736
java.util.stream.ReferencePipeline$Head@568db2f2
java.util.stream.ReferencePipeline$Head@378bf509
================
a1
a2
a3
b1
b2
b3
c1
c2
c3
在流之外的类中你也会发现 flatMap
方法,因为它是计算机科学中的一种通用概念。假设我们有一个泛型 G
(例如 Stream
),以及将某种类型 T
转换为 G<U>
的函数 f
和将类型 U
转换为 G<V>
的函数 g
。然后,我们可以通过使用 flatMap
来组合它们,即首先应用 f
,然后应用 g
。这是单子论的关键概念。但是不必担心,我们无须了解任何有关单子的知识就可以使用 flatMap
| 方法声明 | 描述 |
|
| 产生一个流,它包含当前流中所有满足断言条件的元素 |
|
| 产生一个流, 它包含将 |
|
| 产生一个流,它是通过将 |
抽取子流和连接流
调用 stream.limit(n)
会返回一个新的流,它在 n 个元素之后结束(如果原来的流更短,那么就会在流结束时结束)。这个方法对于裁剪无限流的尺寸会显得特别有用。
Stream<Double> randoms = Stream.generate(Math::random).limit(100);
调用 stream.skip(n)
正好相反:它会丢弃前 n 个元素。如果第一个元素是没什么用的字符串。可以通过调用 skip
来跳过它:
Stream<String> words = Stream.of(contents.split("\\PL+")).skip(1);
可以用 Stream
静态的 concat
方法将两个流连接起来,当然,第一个流不应该是无限 ,否则第二个流永远都不会得到处理的机会
Stream<String> concat = Stream.concat(Common.letters("Hello"), Common.letters("World"));
| 方法声明 | 描述 |
|
| 产生一个流,其 包含了当前流中最初的 |
|
| 产生一个流,它的元素是当前流中除了前 n 个元素之外的所有元素 |
|
| 产生一个流,它的元素是 |
其他的流转换
distinct
方法会返回一个流,它的元素是从原有流中产生的, 原来的元素按照同样的顺序剔除重复元素后产生的 这个流显然能够记住它已经看到过的元素
Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct();
对于流的排序,有多种 sorted
方法的变体可用。其中一种用于操作 Comparable
元素的流,而另一种可以接受一个 Comparator
。下面,我们对字符串排序,使得最长的字符串排在最前面:
Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());
与所有的流转换一样, sorted
方法会产生一个新的流,它的元素是原有流中按照顺序排列的元素。当然 ,我们在对集合排序时可以不使用流。但是,当排序处理是流管道的一部分时,sorted
方法就会显得很有用
peek
方法会产生另一个流,它的元素与原来流中的元素相同,但是在每次获取一个元素时,都会调用一个函数 这对于调试来说很方便:
Object[] power = Stream.iterate(1.0, p -> p * 2)
.peek(e -> System.out.println("Fetching " + e))
.limit(20).toArray();
当实际访问一个元素时,就会打印出来一条消息 通过这种方式,你可以验证 iterate
返回的无限流是被惰性处理的
对于调试,你可以让 peek
调用一个你设置了断点的方法
| 方法声明 | 描述 |
|
| 产生一个流,包含当前流中所有不同的元素 |
|
| 产生一个流,它的元素是当前流中的所有元素按照顺序排列的。第一个方法要求元素是实现了 |
|
| 产生一个流,它与当前流中的元素相同,在获取其中每个元素时,会将其传递给 |
简单约简
约简是一种终结操作( terminal operation ),它们会将流约简为可以在程序中使用的非流值
count
方法会返回流中元素的数量,max
和 min
,会返回最大值和最小值,这些方法返回的是一个类型 Optional<T>
的值,它要么在其中包装了答案,要么表示没有任何值(因为流碰巧为空) 在过去,碰到这种情况返回 null
是很常见的,但是这样做会导致在未做完备测试的程序中产生空指针异常 Optional
类型是一种更好的表示缺少返回值的方
如何获得流中的最大值:
Optional<String> max = words.max(String::compareToIgnoreCase);
System.out.println("max :: " + max.orElse(""));
findFirst
返回的是非空集合中的第一个值。它通常会在与 filter
组合使用时显得很有用。例如,下面展示了如何找到第一个以字母 Q
开头的单词,前提是存在这样的单词:
Optional<String> startWithQ = words.filter(s -> s.startsWith("Q")).findFirst();
System.out.println("startWithQ :: " + startWithQ.orElse(""));
如果不强调使用第一个匹配,而是使用任意的匹配都可以,那么就可以使用 findAny
方法。这个方法在并行处理流时会很有效,因为流可以报告任何它找到的匹配而不是被限制为必须报告第一个匹配。
Optional<String> startWithQ = words.parallel().filter(s -> s.startsWith("Q")).findAny();
System.out.println("startWithQ :: " + startWithQ.orElse(""));
如果只想知道是否存在匹配,那么可以使用 anyMatch
这个方法会接受一个断言引元,因此不需要使用 filter
boolean aWordStartsWithQ = words.parallel().anyMatch(s -> s.startsWith("Q"));
System.out.println("aWordStartsWithQ :: " + aWordStartsWithQ);
还有 allMatch
和 noneMatch
方法,它们分别会在所有元素和没有任何元素匹配断言的情况下返回 true
。这些方法也可以通过并行运行而获益
| 方法声明 | 描述 |
|
| 分别产生这个流的最大元素和最小元素,使用由给定比较器定义的排序规则,如果这个流为空,会产生一个空的 |
|
| 分别产生这个流的第一个和任意一个元素,如果这个流为空,会产生一个空的 |
|
| 分别在这个流中任意元素、所有元素和没有任何元素匹配给定断言时返回 |
Optional
类型
Optional<T>
对象是一种包装器对象,要么包装了类型 T
的对象,要么没有包装任何对象。对于第一种情况,我们称这种值为存在的。Optional<T>
类型被当作一种更安全的方式,用来替代类型 T
的引用 ,这种引用要么引用某个对象,要么为 null
。但是,它只有在正确使用的情况下才会更安全
如何使用 Optional
值
有效地使用 Optional
关键是要使用这样的方法:它在值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值
第一条策略:在没有任何匹配时,我们会希望使用某种默认值、或抛出异常:
// 1. 在没有任何匹配时,使用默认值
String result1 = optionalString.orElse("");
// 2. 在没有任何匹配时,通过计算获得结果
String result2 = optionalString.orElseGet(()-> Locale.getDefault().getDisplayName());
// 3. 在没有任何匹配时,抛出异常
String result3 = optionalString.orElseThrow(IllegalStateException::new);
另一条使用可选值的策略是只有在其存在的情况下才消费该值:
ifPresent
方法会接受一个函数。如果该可选值存在,那么它会被传递给该函数。否则,不会发生任何事情。
optionalString.ifPresent(results::add);
当调用 ifPresent
时, 从该函数不会返回任何值。如果想要处理函数的结果,应该使用 map
,同理,如果该可选值不存在,不会调用 map
:
Optional<Boolean> added = optionalString.map(results::add);
这个 map
方法与 Stream
接口的 map
方法类似。你可以直接将可选值想象成尺寸为 0 或 1 的流。结果的尺寸也是 0 或 1,并且在后一种情况中,会应用到函数
| 方法声明 | 描述 |
|
| 产生这个 |
|
| 产生这个 |
|
| 产生这个 |
|
| 如果该 |
|
| 产生将该 |
不适合使用 Optional
值的方式
如果没有正确使用 Optional
值,那么相比较以往的得到 “某物或 null ” 的方式,你并没有得到任何好处
get
方法会在 Optional
值存在的情况下获得其中包装的元素,或者在不存在的情况下抛出一个 NoSuchElementException
对象
Optional<T> optionalValue = ...;
optionalValue.get().someMethod();
并不比下面的方式更安全:
T value = ...;
value.someMethod();
isPresent
方法会报告某个 Optional<T>
对象是否具有一个值。但是
if (optionalValue.isPresent()) {
optionalValue.get().someMethod();
}
并不比下面的方式更容易处理:
if (value != null) {
value.someMethod();
}
| 方法声明 | 描述 |
|
| 产生这个 |
|
| 如果该 |
创建 Optional
值
ofNullable
方法被用来作为可能出现的 null
值和可选值之间的桥梁。Optional.ofNullable(obj)
会在 obj
不为 null
的情况下返回 Optional.of(obj)
,否则会返回 Optional.empty()
| 方法声明 | 描述 |
|
| 产生一个具有给定值的 |
|
| 产生一个空 |
用 flatMap
来构建 Optional
值的函数
假设你有一个可以产生 Optional<T>
对象的方法 f
,并且目标类型 T
具有一个可以产生 Optional<U>
对象的方法 g
。如果它们都是普通的方法,那么你可以通过调用 s.f().g()
来将它们组合起来。但是这种组合没法工作,因为 s.f()
的类型为 Optional<T>
,而不是 T
。因此,需要调用:
Optional<U> result = s.f().flatMap(T::g);
如果 s.f()
的值存在,那么 g
就可以应用到它上面 ,就会返回一个空 Optional<U>
如果有更多的可以产生 Optional
方法或 Lambda 表达式,那么就可以重复此过程。你可以直接将对 flatMap
的调用链接起来,从而构建由这些步骤构成的管道,只有所有步骤都成功 ,该管道才会成功
Stream
接口中的 flatMap
方法被用来将可以产生流的两个方法组合起来,其实现方式是摊平由流构成的流。如果将可选值当作尺寸为 0 和 1 的流来解释,那么 Optional.flatMap
方法与其操作方式一样
| 方法声明 | 描述 |
|
| 产生将 |
收集结果( collect
)
当处理完流之后,通常会想要查看其元素 此时可以调用 iterator
方法,它会产生可以用来访问元素的旧式风格的迭代器
Iterator<String> iterator = stream.iterator();
或者,可以调用 forEach
方法,将某个函数应用于每个元素:
stream.forEach(System.out::println);
在并行流上, forEach
方法会以任意顺序遍历各个元素 如果想要按照流中的顺序来处理它们,可以调用 forEachOrdered
方法。当然,这个方法会丧失并行处理的部分甚至全部优势。
但是,更常见的情况是,我们想要将结果收集到数据结构中。此时,可以调用 toArray
,获得由流的元素构成的数组。因为无法在运行时创建泛型数组,表达式 stream.toArray()
会返回 Object[]
数组。如果想要让数组具有正确的类型, 可以将其传递到数组构造器中:
Object[] objects = stream.toArray();
String[] strings = stream.toArray(String[]::new);
针对将流中的元素 另一个目标 ,有一个便捷方法 collect
可用,它会接受一个 Collector
接口的实例。Collectors
类提供了大量用于生成公共收集器的工厂方法。为了将流收集到列表或集中,可以直接调用
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
如果想要控制获得的集合的种类,那么可以使用下面的调用:
TreeSet<String> treeSet = stream.collect(Collectors.toCollection(TreeSet::new));
通过连接操作 joining
来收集流中的所有字符串,如果想要在元素之间增加分隔符,可以将分隔符传递给 joining
方法:
String result = stream.collect(Collectors.joining());
String result = stream.collect(Collectors.joining(","));
String result = stream.map(Object::toString).collect(Collectors.joining(","));
如果想要将流的结果约简为总和、平均值、最大值或最小值,可以使 summarizing
(Int
| Long
| Double
) 方法中的某一个。这些方法会接受一个将流对象映射为数据的函数,同时,这些方法会产生类型为 ( Int
| Long
| Double
) SummaryStatistics
结果, 同时计算总和、数量、平均值、最大值和最小值
Stream<String> stream = Stream.of("a", "b", "c", "d", "e", "f", "g");
IntSummaryStatistics summary = stream.collect(Collectors.summarizingInt(String::length));
System.out.println(summary); // IntSummaryStatistics{count=7, sum=7, min=1, average=1.000000, max=1}
double average = summary.getAverage();
int max = summary.getMax();
| 方法声明 | 描述 |
|
| 产生一个用于获取当前流中各个元素的迭代器。 这是 终结操作 |
| 方法声明 | 描述 |
|
| 在流的每个元素上调用 |
|
| 产生一个对象数组,或者在将引用 |
|
| 使用给定的收集器来收集当前流中的元素。 |
| 方法声明 | 描述 |
|
| 产生一个将元素收集到列表或集中的收集器 |
|
| 产生一个将元素收集到任意集合中的收集器。可以传递一个诸如 |
|
| 产生一个连接字符串的收集器。分隔符会置于字符串之间,而第一个字符串之前可以有前缀,最后一个字符串之后可以有后缀。如果没有指定,那么它们都为空 |
|
| 产生能够生成 ( |
| 方法声明 | 描述 |
|
| 产生汇总后的元素的个数 |
|
| 产生汇总后的元素的总和或平均值,或者在没有 何元素时返回 0 |
|
| 产生汇总后的元素的最大值和最小值,或者在没有任何元素时,产生 |
java.util.LongSummaryStatistics
、java.util.DoubleSummaryStatistics
具有与 java.util.IntSummaryStatistics
相同的方法
收集到映射表中( toMap
)
Collectors.toMap
方法有两个函数引元,用来产生映射表的键和值。
Function.identity()
出参与入参相同,返回入参本身
如果有多个元素具有相同的键,那么就会存在冲突,收集器将会抛出 IllegalStateException
对象。可以通过提供第 3 个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突并确定键对应的值。这个函数应该返回已有值、新值或它们的组合。
如果想要得到 TreeMap
,那么可以将构造器作为第 4 个引元来提供。你必须提供一种合并函数。
// Function.identity()
Map<String, Locale> map1 = Stream.of(Locale.getAvailableLocales())
.collect(Collectors.toMap(Locale::toString, Function.identity()));
System.out.println(map1);
//
Map<String, String> map2 =
Stream.of(Locale.getAvailableLocales())
.collect(Collectors.toMap(Locale::toString, Locale::getDisplayLanguage));
System.out.println(map2);
// 解决重复键值问题
Map<String, String> map3 = Stream.of(Locale.getAvailableLocales()).collect(
Collectors.toMap(Locale::getDisplayLanguage, Locale::getDisplayLanguage,
(existingValue, newValue) -> existingValue));
System.out.println(map3);
// 第3个参数控制返回重复键的多个值怎么处理
Map<String, Set<String>> map4 = Stream.of(Locale.getAvailableLocales()).collect(
Collectors.toMap(Locale::getDisplayCountry, l -> Collections.singleton(l.getDisplayLanguage()),
(a, b) -> {
Set<String> union = new HashSet<>(a);
union.addAll(b);
return union;
}));
System.out.println(map4);
// 第4个参数控制返回类型
Map<String, String> map5 = Stream.of(Locale.getAvailableLocales()).collect(
Collectors.toMap(Locale::getDisplayLanguage, Locale::getDisplayLanguage,
(existingValue, newValue) -> existingValue, TreeMap::new));
System.out.println(map5.getClass());
对于每一个 toMap
方法,都有一个等价的可以产生并发映射表的 toConcurrentMap
方法,单个并发映射表可以用于并行集合处理。当使用并行流时,共享的映射表比合并映射表要更高效。注意,元素不再是按照流中的顺序收集的,但是通常这不会有什么问题
| 方法声明 | 描述 |
|
| 产生一个收集器,它会产生一个映射表或并发映射表。 |
群组和分区( groupingBy
、partitioningBy
)
每个 Locale
都有一个语言代码(例如英语的 en
)和一个国家代码(例如美国的 US
)。en_US
描述的是美国英语,而 en_IE
是爱尔兰英语。某些国家有多个 Locale
(瑞士有三个 Locale
)。
当分类函数是断言函数(即返回 boolean
值的函数)时,流的元素可以分区为两个列表:该函数返回 true
元素和其他的元素。在这种情况下,使用 partitioningBy
比使用 groupingBy
要更高效。
如果调用 groupingByConcurrent
方法,就会在使用并行流时获得一个被并行组装的并行映射表。这与 toConcurrentMap
方法完全类似
Map<String, List<Locale>> map1 =
Stream.of(Locale.getAvailableLocales()).collect(Collectors.groupingBy(Locale::getCountry));
System.out.println(map1);
Map<Boolean, List<Locale>> map2 = Stream.of(Locale.getAvailableLocales())
.collect(Collectors.partitioningBy(l -> l.getLanguage().equals("en")));
System.out.println(map2);
System.out.println(map2.get(true));
| 方法声明 | 描述 |
|
| 产生一个收集器,它会产生一个映射表或并发映射表,其键是将 |
|
| 产生一个收集器,它会产生一个映射表,其键是 |
下游收集器
groupingBy
方法会产生一个映射表,它的每个值都是一个 List
。如果想要以某种方式来处理这些列表,就需要提供一个“下游收集器”
获得 Set
而不是 List
:
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(Collectors.groupingBy(
Locale::getCountry, Collectors.toSet()));
Java 提供了多种可以将群组元素约简为数字的收集器:
-
counting
会产生收集到的元素的个数 -
summing ( Int | Long | Double )
会接受一个函数作为引元,将该函数应用到下游元素中,并产生它们的和 -
maxBy
,minBy
会接受一个比较器,并产生下游元素中的最大值和最小值 -
mapping
方法会产生将函数应用到下游结果上的收集器,并将函数值传递给另一个收集中
// counting
Map<String, Long> countryToLocaleCounts = locales.collect(Collectors.groupingBy(
Locale::getCountry, Collectors.counting()));
// summingInt
Map<String, Integer> stateToCityPopulation = cities.collect(Collectors.groupingBy(
City::getState, Collectors.summingInt(City::getPopulation)));
// mapping 、maxBy
Map<String, Optional<String>> stateToLongestCityName = cities
.collect(Collectors.groupingBy(
City::getState,
Collectors.mapping(City::getName,
Collectors.maxBy(Comparator.comparing(String::length)))));
// mapping 、toSet
Map<String, Set<String>> countryToLanguages = locales.collect(Collectors.groupingBy(
Locale::getDisplayCountry,
Collectors.mapping(Locale::getDisplayLanguage, Collectors.toSet())));
// groupingBy 、summarizingInt
Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities
.collect(Collectors.groupingBy(City::getState,
Collectors.summarizingInt(City::getPopulation)));
// reducing
Map<String, String> stateToCityNames = cities.collect(Collectors.groupingBy(
City::getState,
Collectors.reducing("", City::getName, (s, t) -> s.length() == 0 ? t : s
+ ", " + t)));
将收集器组合起来是一种很强大的方式,但是它也可能会导致产生非常复杂的表达式。它们的最佳用法是与 groupingBy
和 partitioningBy
一起处理 “下游的” 映射表中的值。否则,应该直接在流上应用诸如 map
、reduce
、count
、max
、min
这样的方法
| 方法声明 | 描述 |
|
| 产生一个可以对收集到的元素进行计数的收集器 |
|
| 产生一个收集器,对将 |
|
| 产生一个收集器,使用 |
|
| 产生一个收集器,它会产生一个映射表,其键是将 |
约简操作( reduce
)
reduce
方法是一种用于从流中计算某个值的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它
List<Integer> values = ...;
Optional<Integer> sum = values.stream().reduce((x, y) -> x + y);
通常,如果 reduce
方法有一项约简操作 op
,那么该约简就会产生 v0 op v1 op v2 op ...
,其中我们将函数调用 op(vi, vi+1)
写作 vi op vi+1
。这项操作应该是可结合的:即组合元素时使用的顺序不应该成为问题。在数学标记法中,(x op y) op z
必须等于 x op (y op z)
。这使得在使用并行流时,可以执行高效的约简
有很多种在实践中会显得很有用的可结合操作,例如求和、乘积、字符串连接、取最大值和最小值、求集的并与交等。减法是一个不可结合操作的例子,例如,(6-3)-2
不等于 6-(3-2)
通常,会有一个幺元值 e
使得 e op x = x
,可以使用这个元素作为计算的起点。例如,0
是加法的幺元值。然后,可以调用第二种形式的 reduce
,如果流为空, 会返回幺元值:
Integer sum = values.stream().reduce(0, (x, y) -> x + y);
现在,假设你有一个对象流,并且想要对某些属性求和,例如字符串流中的所有字符串的长度,那么你就不能使用简单形式的 reduce
,而是需要 (T, T) -> T
这样的函数,即引元和结果的类型相同的函数。首先,你需要提供一种“累积器”函数 (total, word) -> total + word.length()
。这个函数会被反复调用,产生累积的总和。但是,当计算被并行化时,会有多个这种类型的计算,你需要将它们的结果合并。因此,你需要提供第二个函数来执行此处理
int total = words.stream().reduce(0, (total, word) -> total + word.length(), (total1, total2) -> total1 + total2);
在实践中,你可能并不会频繁地用到 reduce
方法。通常,映射为数字流并使用其方法来计算总和、最大值和最小值会更容易
有时 reduce
会显得并不够通用。例如,假设我们想要收集 BitSet
的结果。如果收集操作是并行的,那么就不能直接将元素放到单个 BitSet
中,因为 BitSet
对象不是线程安全的。因此,我们不能使用 reduce
,因为每个部分都需要以其自己的空集开始,并且 reduce
只能让我们提供幺元值。此时,应该使用 collect
,它会接受单个引元:
- 一个提供者,它会创建目标类型的新实例,例如散列集的构造器
- 一个累积器,它会将一个元素添加到一个实例上,例如
add
方法 - 一个组合器,它会将两个实例合并成一个,例如
addAll
BitSet result = words.stream().collect(BitSet::new, BitSet::or, BitSet::or);
| 方法声明 | 描述 |
|
| 用给定的 |
|
| 将元素收集到类型 R 的结果中。在每个部分上,都会调用 |
基本类型流
可以将整数收集到 Stream<Integer>
中,但是将每个整数都包装到包装器对象中是很低效的。对其他基本类型来说,情况也是一样,这些基本类型是: double
、float
、long
、short
、char
、byte
、boolean
。流库中具有专门的类型 IntStream
、LongStream
、DoubleStream
,用来直接存储基本类型值,而无需使用包装器。如果想要存储 short
、char
、byte
、boolean
,可以使用 IntStream
,而对于 float
,可以使用 DoubleStream
为了创建 IntStream
,需要调用 IntStream.of
、Arrays.stream
方法:
IntStream stream = IntStream.of(1, 1, 2, 3, 5);
IntStream stream = Arrays.stream(arr);
IntStream stream = Arrays.stream(arr, from, to);
与对象流一样,我们还可以使用静态的 generate
、iterate
方法。此外 IntStream
和 LongStream
有静态方法 range
、rangeClosed
,可以生成步长为 1 的整数范围:
IntStream zeroToNinetyNine = IntStream.range(0, 100); // [0, 100)
IntStream zeroToHundred = IntStream.rangeClosed(0, 100); // [0, 100]
CharSequence
接口拥有 codePoints
和 chars
方法,可以生成由字符的 Unicode 码或由 UTF-16 编码机制的码元构成的 IntStream
IntStream codes = sentence.codePoints();
IntStream chars = sentence.chars();
当你有 个对象流时,可以用 mapToInt
、mapToLong
、mapToDouble
将其转换为基本类型流。例如,如果你有一个字符串流,并想将其长度处理为整数,那么就可以在 IntStream 中实现:
Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);
为了将基本类型流转换为对象流,需要使 boxed
方法:
Stream<Integer> boxed = IntStream.range(0, 100).boxed();
通常,基本类型流上的方法与对象流上的方法类似。下面是最主要的差异:
-
toArray
方法会返回基本类型数组 - 产生可选结果的方法会返回一个
OptionalInt
、OptionalLong
、OptionalDouble
。这些类与Optional
类类似,但是具有getAsInt
、getAsLong
、getAsDouble
方法,而不是get
方法 - 具有返回总和、平均值、最大值和最小值的
sum
、average
、max
、min
方法。对象流没有定义这些方法 -
summaryStatistics
方法会产生一个类型为IntSummaryStatistics
、LongSummaryStatistics
、DoubleSummaryStatistics
的对象,它们可以同时报告流的总和、平均值、最大值和最小值
Random
类具有 ints
、longs
、doubles
方法,它们会返回由随机数构成的基本类型流
Random random = new Random();
IntStream intStream = random.ints();
LongStream longStream = random.longs();
DoubleStream doubleStream = random.doubles();
| 方法声明 | 描述 |
|
| 产生一个由给定范围内的整数构成的 |
|
| 产生一个由给定元素构成的 |
|
| 产生一个由当前流中的元素构成的数组 |
|
| 产生当前流中元素的总和、平均值、最大值和最小值,或者从中可以获得这些结果的所有四种值的对象 |
|
| 产生用于当前流中的元素的包装器对象流 |
LongStream
、DoubleStream
和 IntStream
具有相同的方法
| 方法声明 | 描述 |
|
| 产生由当前字符串的所有 Unicode 码点构成的流 |
| 方法声明 | 描述 |
|
| 产生随机数流 果提供了 |
longs
、doubles
和 ints
具有相同的方法
| 方法声明 | 描述 |
|
| 用所提供的基本类型值产生一个可选对象 |
|
| 产生当前可选对象的值,或者在其为空时抛出一个 |
|
| 产生当前可选对象的值,或者在这个对象为空时产生可替代的值,或者抛出异常 |
|
| 如果当前可选对象不为空, 将其值传递给 |
OptionalLong
、OptionalDouble
和 OptionalInt
具有相同的方法
| 方法声明 | 描述 |
|
| 产生收集到的元素的个数、总和、平均值、最大值和最小值 |
LongSummaryStatistics
、DoubleSummaryStatistics
和 IntSummaryStatistics
具有相同的方法
并行流
流使得并行处理块操作变得很容易。这个过程几乎是自动的,但是需要遵守一些规则
可以用 Collection.parallelStream()
方法从任何集合中获取一个并行流
Stream<String> parallelStream = words.parallelStream();
Stream.parallel
方法可以将任意的顺序流转换为并行流
Stream<String> parallelStream = stream.parallel();
只要在终结方法执行时,流处于并行模式,那么所有的中间流操作都将被井行化
当流操作并行运行 ,其目标是要让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作可以以任意顺序执行
假设要统计流中所有短单次的个数,
并行流的错误用法:
int[] shortWords = new int[12];
Stream.of(contents.split("\\PL+")).parallel().forEach(
s -> {
if (s.length() < 12) {
shortWords[s.length()]++;
}
});
并行流的正确用法:
Map<Integer, Long> shortWordCounts =
Stream.of(contents.split("\\PL+")).parallel()
.filter(s -> s.length() < 10)
.collect(Collectors.groupingBy(
String::length,
Collectors.counting()));
警告:传递给并行流操作的函数不应该被堵塞 并行流使用 fork-join 池来操作流的各个部。 如果多个流操作被阻塞,那么池可能就无法做任何事情了
默认情况下,从有序集合(数组和列表)、范围、生成器和迭代产生的流,或者通过调用 Stream.sorted
产生的流,都是有序的 它们的结果是按照原来元素的顺序累积的,因此是完全可预知的。如果运行相同的操作两次,将会得到完全相同的结果
排序并不排斥高效的并行处理 例如,当计算 stream.map(fun)
时,流可以被划分为 n 部分,它们会被并行地处理。然后,结果将会按照顺序重新组装起来
当放弃排序需求时,有些操作可以被更有效地并行化。通过在流上调用 unordered
方法,就可以明确表示我们对排序不感兴趣。 Stream.distinct
就是从这种方式中获益的一种操作。在有序的流中, distinct
会保留所有相同元素中的第一个,这对并行化是 种阻碍,因为处理每个部分的线程在其之前的所有部分都被处理完之前,并不知道应该丢弃哪些元素。如果可以接受保留唯一元素中任意一个的做法,那么所有部分就可以并行地处理(使用共享的集来跟踪重复元素)
还可以通过放弃排序要求来提高 limit
方法的速度
Stream<String> sample = words.parallel().unordered().limit(n);
合并映射表的代价很高昂 正是因为这个原因, Collectors.groupByConcurrent
方法使用了共享的并发映射表。为了从并行化中获益,映射表中值的顺序不会与流中的顺序相同
Map<Integer, List<String>> result = words.parallel().collect(Collectors.groupingByConcurrent(String::length));
当然,如果使用独立于排序的下游收集器,那么就不必在意了
Map<Integer, Long> wordCounts =
words.parallel().collect(Collectors.groupingByConcurrent(String::length, Collectors.counting()));
警告:不要修改在执行某项流操作后会将元素返回到流中的集合 (即使这种修改是线程安全的) 记住,流并不会收集它们的数据,数据总是在单独的集合中。如果修改了这样的集合,那么流操作的结果就是未定义的 JDK 文档对这项需求并未做出任何约束,并且对顺序流和并行流都采用了这种处理方式
更准确地讲,因为中间的流操作都是惰性的,所以直到执行终结操作时才对集合进行修改仍旧是可行的
// 可以工作,但是不推荐
List<String> wordList = ...;
Stream<String> words = wordList.stream();
wordList.add("END");
long n = words.distinct().count();
// 错误
List<String> wordList = ...;
Stream<String> words = wordList.stream();
words.forEach(s -> {
if (s.length() < 12) {
wordList.remove(s);
}
});
为了让并行流正常工作,需要满足大量的条件:
- 数据应该在内存中。必须等到数据到达是非常低效的
- 流应该可以被高效地分成若干个子部分。由数组或平衡二叉树支撑的流都可以工作得很好,但是
Stream.iterate
返回的结果不行 - 流操作的工作量应该具有较大的规模。如果总工作负载并不是很大,那么搭建并行计算时所付出的代价就没有什么意义
- 流操作不应该被阻
换句话说,不要将所有的流都转换为并行流。只有在对已经位于内存中的数据执行大量计算操作时,才应该使用并行流
| 方法声明 | 描述 |
|
| 产生一个与当前流中元素相同的并行流 |
|
| 产生一个与当前流中元素相同的无序流 |
| 方法声明 | 描述 |
|
| 用当前集合中的元素产生一个并行流 |