mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-19 12:33:29 +08:00
@@ -1668,10 +1668,451 @@ Signal(dash)
|
||||
|
||||
## 终端操作
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
|
||||
这些操作获取一个流并产生一个最终结果;它们不会像后端流提供任何东西。因此,终端操作总是你在管道中做的最后一件事情。
|
||||
|
||||
### 转化成数组(Convert to an Array)
|
||||
|
||||
- `toArray()`:将流转换成适当类型的数组。
|
||||
- `toArray(generator)`:在特殊情况下,生成器用于分配你自己的数组存储。
|
||||
|
||||
如果流操作产生的结果必须使用的是数组形式,这是非常有用的。例如,假设我们想要以一种我们可以将它们作为流重用的方式捕获随机数,这样我们每次都可以得到相同的流。 我们可以通过将它们存储在一个数组中来实现:
|
||||
|
||||
```java
|
||||
// streams/RandInts.java
|
||||
package streams;
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
public class RandInts {
|
||||
private static int[] rints = new Random(47).ints(0, 1000).limit(100).toArray();
|
||||
public static IntStream rands() {
|
||||
return Arrays.stream(rints);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
一个包含 100 个数值范围在 0 到 10000 之间的随机数流转换成为数组并将其存储在 **rints** 中,所以在每次调用 `rands()` 的时候你都可以重复的获取相同的流。
|
||||
|
||||
### 每一个元素应用最终操作(Apply a Final Operation to Every Element)
|
||||
|
||||
- `forEach(Consumer)`:你已经看到很多次 `System.out::println` 作为 **Consumer** 函数。
|
||||
- `forEachOrdered(Consumer)`: 这个版本保证了 forEach 的操作顺序是原始流顺序。
|
||||
|
||||
第一种形式是显示的设计为以任何顺序操作元素,这只在引入 `parallel()` 操作时才有意义。在 [并发]() 章节之前我们不会深入研究这个问题,但是这里有一个简单的介绍:`parallel()` 告诉 Java 尝试在多个处理器上运行操作。它可以做到这一点是因为我们使用流——流可以被分割为多个流(通常一个流一个处理器)并且每个流运行在不同的处理器上。因为我们使用的是内部循环而不是外部循环,这是可行的。
|
||||
|
||||
在你对 `parallel()` 的看似容易感到过于兴奋之前,它实际上相当棘手,所以请等到我们进入 [并发]() 编程章节。
|
||||
|
||||
但是,我们可以通过将 `parallel()` 引入一个示例来了解 `forEachOrdered(Consumer)` 的效果和需求:
|
||||
|
||||
```java
|
||||
// streams/ForEach.java
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
import static streams.RandInts.*;
|
||||
public class ForEach {
|
||||
static final int SZ = 14;
|
||||
public static void main(String[] args) {
|
||||
rands().limit(SZ)
|
||||
.forEach(n -> System.out.format("%d ", n));
|
||||
System.out.println();
|
||||
rands().limit(SZ)
|
||||
.parallel()
|
||||
.forEach(n -> System.out.format("%d ", n));
|
||||
System.out.println();
|
||||
rands().limit(SZ)
|
||||
.parallel()
|
||||
.forEachOrdered(n -> System.out.format("%d ", n));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
258 555 693 861 961 429 868 200 522 207 288 128 551 589
|
||||
551 861 429 589 200 522 555 693 258 128 868 288 961 207
|
||||
258 555 693 861 961 429 868 200 522 207 288 128 551 589
|
||||
```
|
||||
|
||||
我已经隔离了 sz,以便轻松尝试不同的尺寸。 然而,即使是 14 的 sz 也会产生有趣的结果。在第一个流中,我们没有使用 `parallel()` 所以按照它们从 `rands()` 出现的顺序显示结果。第二个流我们引入了 `parallel()` ,即使是很小的流,你也会看到输出结果和之前的顺序并不一样。这是由于多个处理器正在解决这个问题,如果你多次运行这个程序,你将会看到这个结果均是不同的,由于多个处理器同时处理该问题而产生的非确定性因素。
|
||||
|
||||
最后一个流依旧使用了 `parallel()`,但是通过使用 `forEachOrdered()` 强制结果保留原始顺序。因此使用 `parallel()` 对于非并行流没有任何影响。
|
||||
|
||||
### 收集(Collecting)
|
||||
|
||||
- `collect(Collector)`:使用 **Collector** 来累计流元素到结果集合中。
|
||||
- `collect(Supplier, BiConsumer, BiConsumer)`:同上,但是 **Supplier** 创建了一个新的结果集合,第一个 **BiConsumer** 是将下一个元素包含在结果中的函数,而第二个 **BiConsumer** 是用于将两个值组合起来。
|
||||
|
||||
你只看到了少数几个 **Collectors** 对象的示例。如果你查看 ` java.util.stream.Collectors`的文档,你会发现其中的一些实现非常复杂。例如,我们可以将元素收集到任意一种特定的集合中。假设我们想将我们的元素最终在 **TreeSet** 中,以保证它们总是有序的。在 **Collectors** 里面没有特定的 **toTreeSet()** 方法,但是你可以使用 ` Collectors.toCollection()`并为任何类型的Collection提供构造函数引用。 该程序将文件中的单词拉入**TreeSet** :
|
||||
|
||||
```java
|
||||
// streams/TreeSetOfWords.java
|
||||
import java.util.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.stream.*;
|
||||
public class TreeSetOfWords {
|
||||
public static void
|
||||
main(String[] args) throws Exception {
|
||||
Set<String> words2 =
|
||||
Files.lines(Paths.get("TreeSetOfWords.java"))
|
||||
.flatMap(s -> Arrays.stream(s.split("\\W+")))
|
||||
.filter(s -> !s.matches("\\d+")) // No numbers
|
||||
.map(String::trim)
|
||||
.filter(s -> s.length() > 2)
|
||||
.limit(100)
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
System.out.println(words2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
[Arrays, Collectors, Exception, Files, Output, Paths,
|
||||
Set, String, System, TreeSet, TreeSetOfWords, args,
|
||||
class, collect, file, filter, flatMap, get, import,
|
||||
java, length, limit, lines, main, map, matches, new,
|
||||
nio, numbers, out, println, public, split, static,
|
||||
stream, streams, throws, toCollection, trim, util,
|
||||
void, words2]
|
||||
```
|
||||
|
||||
`Files.lines()` 打开 **Path** 并将其转换成为行流。下一行代码将根据一个或者多个非单词字符(\\\\w+)作为边界对行进行分割,然后使用 `Arrays.stream()` 将其转化成为流,并将结果展平映射成为单词流。`matches(\\d+)` 寻找并移除那些全是数字的字符串(注意 **words2** 是通过的)。接下来我们使用 `String.trim()` 去除单词两边的空白,`filter()`过滤所有长度小于 3 的单词,然后只获取 100 个单词,并最终将其塞入到 **TreeSet** 中。
|
||||
|
||||
我们也可以在流中生成 **Map**:
|
||||
|
||||
```java
|
||||
// streams/MapCollector.java
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
class Pair {
|
||||
public final Character c;
|
||||
public final Integer i;
|
||||
Pair(Character c, Integer i) {
|
||||
this.c = c;
|
||||
this.i = i;
|
||||
}
|
||||
public Character getC() { return c; }
|
||||
public Integer getI() { return i; }
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Pair(" + c + ", " + i + ")";
|
||||
}
|
||||
}
|
||||
class RandomPair {
|
||||
Random rand = new Random(47);
|
||||
// An infinite iterator of random capital letters:
|
||||
Iterator<Character> capChars = rand.ints(65,91)
|
||||
.mapToObj(i -> (char)i)
|
||||
.iterator();
|
||||
public Stream<Pair> stream() {
|
||||
return rand.ints(100, 1000).distinct()
|
||||
.mapToObj(i -> new Pair(capChars.next(), i));
|
||||
}
|
||||
}
|
||||
public class MapCollector {
|
||||
public static void main(String[] args) {
|
||||
Map<Integer, Character> map =
|
||||
new RandomPair().stream()
|
||||
.limit(8)
|
||||
.collect(
|
||||
Collectors.toMap(Pair::getI, Pair::getC));
|
||||
System.out.println(map);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N}
|
||||
```
|
||||
|
||||
**Pair** 只是一个基础的数据对象。**RandomPair** 创建了随机生成的 **Pair** 对象流。如果我们能以某种方式组合两个流, 那就再好不过了, 但 Java 在这个问题上与我们斗争。所以我创建了一个整数流,并且使用 `mapToObj` 将其转化成为 **Pair** 流。 **capChars** 随机生成的大写字母迭代器从流开始,然后 `iterator()` 方法允许我们在 `stream()` 方法中使用它。就我所知, 这是组合多个流以生成新的对象流的唯一方法。
|
||||
|
||||
在这里,我们只使用最简单形式的 `Collectors.toMap()`,这个方法值需要一个可以从流中获取键值对的函数。但是这还有另外一种形式,需要一个函数来处理键碰撞的情况。
|
||||
|
||||
在大多数情况下,你可以在 `java.util.stream.Collectors`寻找到你想要的预先定义好的 **Collector**。在少数情况下当你找不到想要的时候,你可以使用第二种形式的 ` collect()`。 I’ll basically leave that as a more advanced exercise, 但是这里有一个例子给出了基本想法:
|
||||
|
||||
```java
|
||||
// streams/SpecialCollector.java
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
public class SpecialCollector {
|
||||
public static void main(String[] args) throws Exception {
|
||||
ArrayList<String> words =
|
||||
FileToWords.stream("Cheese.dat")
|
||||
.collect(ArrayList::new,
|
||||
ArrayList::add,
|
||||
ArrayList::addAll);
|
||||
words.stream()
|
||||
.filter(s -> s.equals("cheese"))
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
cheese
|
||||
cheese
|
||||
```
|
||||
|
||||
在这里, **ArrayList** 的方法已经执行了你所需要的操作,但是似乎更有可能的是,如果你必须使用这种形式的 `collect()`,则必须自己创建特殊的定义。
|
||||
|
||||
### 组合所有流元素(Combining All Stream Elements)
|
||||
|
||||
- `reduce(BinaryOperator)`:使用 **BinaryOperator** 来组合所有流中的元素。因为流可能为空,其返回值为 **Optional**。
|
||||
- `reduce(identity, BinaryOperator)`:功能同上,但是使用 **identity** 作为其组合的初始值。因此如果流为空,**identity** 就是结果。
|
||||
- `reduce(identity, BiFunction, BinaryOperator)`:这个版本更为复杂(所以我们不会介绍它),在这里被提到是因为它使用起来会更有效。通常,你可以显示的组合 `map()` 和 `reduce()` 来更简单的表达这一点。
|
||||
|
||||
如下是一个用于演示 `reduce()` 的示例:
|
||||
|
||||
```java
|
||||
// streams/Reduce.java
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
class Frobnitz {
|
||||
int size;
|
||||
Frobnitz(int sz) { size = sz; }
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Frobnitz(" + size + ")";
|
||||
}
|
||||
// Generator:
|
||||
static Random rand = new Random(47);
|
||||
static final int BOUND = 100;
|
||||
static Frobnitz supply() {
|
||||
return new Frobnitz(rand.nextInt(BOUND));
|
||||
}
|
||||
}
|
||||
public class Reduce {
|
||||
public static void main(String[] args) {
|
||||
Stream.generate(Frobnitz::supply)
|
||||
.limit(10)
|
||||
.peek(System.out::println)
|
||||
.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
|
||||
.ifPresent(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
Frobnitz(58)
|
||||
Frobnitz(55)
|
||||
Frobnitz(93)
|
||||
Frobnitz(61)
|
||||
Frobnitz(61)
|
||||
Frobnitz(29)
|
||||
Frobnitz(68)
|
||||
Frobnitz(0)
|
||||
Frobnitz(22)
|
||||
Frobnitz(7)
|
||||
Frobnitz(29)
|
||||
```
|
||||
|
||||
**Frobnitz** 包含了一个名为 `supply()` 的生成器;因为这个方法对于 **Supplier\<Frobnitz\>** 是签名兼容的,我们可以将其方法引用传递给 `Stream.generate()`(这种签名兼容性被称作结构一致性)。我们使用没有给“起始值”的 `reduce()`方法,这意味着它的返回值是 **Optional** 类型的。`Optional.ifPresent()` 只有在结果非空的时候才会调用 **Consumer\<Frobnitz\>** (`println` 方法可以被调用是因为 **Frobnitz** 可以通过 `toString()` 方法转换成 **String**)。
|
||||
|
||||
Lambda 表达式中的第一个参数 **fr0** 是上一次调用 `reduce()` 的结果。而第二个参数 **fr1** 是从流传递过来值。
|
||||
|
||||
`reduce()` 中的 Lambda 表达式使用了三元表达式来获取结果,当其 size 大于 50 的时候获取 **fr0** 或者获取序列中的下一个值 **fr1**。因此你会取得第一个 size 小于 50 的 **Frobnitz**,只要找到了就这个结果就会紧紧的攥住它,即使有其他候选者也会出现。虽然这是一个非常奇怪的约束,但是它确实让你对 `reduce()` 有了更多的了解。
|
||||
|
||||
### 匹配(Matching)
|
||||
|
||||
- `allMatch(Predicate)` :如果流的每个元素根据提供的 **Predicate** 都返回 true 时,结果返回为 true。这个操作将会在第一个 false 之后短路;也就是不会在发生 false 之后继续执行计算。
|
||||
- `anyMatch(Predicate)`:如果流中的一个元素根据提供的 **Predicate** 返回 true 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。
|
||||
- `noneMatch(Predicate)`:如果流的每个元素根据提供的 **Predicate** 都返回 false 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。
|
||||
|
||||
你已经在 **Prime.java** 中看到了 `noneMatch()` 的示例;` allMatch()` 和 `anyMatch()` 的用法基本上是等同的。让我们探究短路行为。为了创建消除冗余代码的 ` show()` 方法,我们必须首先发现如何概括地描述所有三个匹配器操作, 然后将其转换为称为 **Matcher** 的接口:
|
||||
|
||||
```java
|
||||
// streams/Matching.java
|
||||
// Demonstrates short-circuiting of *Match() operations
|
||||
import java.util.stream.*;
|
||||
import java.util.function.*;
|
||||
import static streams.RandInts.*;
|
||||
|
||||
interface Matcher extends BiPredicate<Stream<Integer>, Predicate<Integer>> {}
|
||||
|
||||
public class Matching {
|
||||
static void show(Matcher match, int val) {
|
||||
System.out.println(
|
||||
match.test(
|
||||
IntStream.rangeClosed(1, 9)
|
||||
.boxed()
|
||||
.peek(n -> System.out.format("%d ", n)),
|
||||
n -> n < val));
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
show(Stream::allMatch, 10);
|
||||
show(Stream::allMatch, 4);
|
||||
show(Stream::anyMatch, 2);
|
||||
show(Stream::anyMatch, 0);
|
||||
show(Stream::noneMatch, 5);
|
||||
show(Stream::noneMatch, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
1 2 3 4 5 6 7 8 9 true
|
||||
1 2 3 4 false
|
||||
1 true
|
||||
1 2 3 4 5 6 7 8 9 false
|
||||
1 false
|
||||
1 2 3 4 5 6 7 8 9 true
|
||||
```
|
||||
|
||||
**BiPredicate** 是一个二元谓词,这意味着它只能接受两个参数并且只返回 true 或者 false。它的第一个参数使我们要测试的流,第二个参数是一个谓词 **Predicate**。因为 **Matcher** 适用于所有的 **Stream::*Match** 方法形式,我们可以传递每一个到 `show()` 中。`match.test()` 的调用会被转换成 **Stream::*Match** 函数的调用。
|
||||
|
||||
`show()` 获取两个参数,**Matcher** 匹配器和用于表示谓词测试 **n < val** 中最大值的 **val**。这个方法生成一个从 1 到 9 的整数流。`peek()` 是用于像我们展示测试在短路之前的情况。你可以在输出中发现每一次短路都会发生。
|
||||
|
||||
### 选择元素(Selecting an Element)
|
||||
|
||||
- `findFirst()`:返回一个含有第一个流元素的 **Optional**,如果流为空返回 **Optional.empty**。
|
||||
- `findAny(`:返回含有任意流元素的 **Optional**,如果流为空返回 **Optional.empty**。
|
||||
|
||||
```java
|
||||
// streams/SelectElement.java
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
import static streams.RandInts.*;
|
||||
public class SelectElement {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(rands().findFirst().getAsInt());
|
||||
System.out.println(
|
||||
rands().parallel().findFirst().getAsInt());
|
||||
System.out.println(rands().findAny().getAsInt());
|
||||
System.out.println(
|
||||
rands().parallel().findAny().getAsInt());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
258
|
||||
258
|
||||
258
|
||||
242
|
||||
```
|
||||
|
||||
`findFirst()` 无论流是否为并行化的,总是会选择流中的第一个元素。对于非并行流,`findAny()`会选择流中的第一个元素(即使从定义上来看是选择任意元素)。在这个例子中,我们使用 `parallel()` 来并行流从而引入 `findAny()` 选择非第一个流元素的可能性。
|
||||
|
||||
如果必须选择流中最后一个元素,那就使用 ` reduce()`:
|
||||
|
||||
```java
|
||||
// streams/LastElement.java
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
public class LastElement {
|
||||
public static void main(String[] args) {
|
||||
OptionalInt last = IntStream.range(10, 20)
|
||||
.reduce((n1, n2) -> n2);
|
||||
System.out.println(last.orElse(-1));
|
||||
// Non-numeric object:
|
||||
Optional<String> lastobj =
|
||||
Stream.of("one", "two", "three")
|
||||
.reduce((n1, n2) -> n2);
|
||||
System.out.println(
|
||||
lastobj.orElse("Nothing there!"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
19
|
||||
three
|
||||
```
|
||||
|
||||
`reduce()` 的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果为数字流,你必须使用相近的数字可选类型( numeric optional type),否则你使用的 Optional 类型为 **Optional\<String\>**。
|
||||
|
||||
### 信息(Informational)
|
||||
|
||||
- `count()`:流中的元素个数。
|
||||
- `max(Comparator)`:根据所传入的 **Comparator** 所决定的“最大”元素。
|
||||
- `min(Comparator)`:根据所传入的 **Comparator** 所决定的“最小”元素。
|
||||
|
||||
字符串类型有预先定义好的 **Comparator**,这简化了我们的示例:
|
||||
|
||||
```java
|
||||
// streams/Informational.java
|
||||
import java.util.stream.*;
|
||||
import java.util.function.*;
|
||||
public class Informational {
|
||||
public static void
|
||||
main(String[] args) throws Exception {
|
||||
System.out.println(
|
||||
FileToWords.stream("Cheese.dat").count());
|
||||
System.out.println(
|
||||
FileToWords.stream("Cheese.dat")
|
||||
.min(String.CASE_INSENSITIVE_ORDER)
|
||||
.orElse("NONE"));
|
||||
System.out.println(
|
||||
FileToWords.stream("Cheese.dat")
|
||||
.max(String.CASE_INSENSITIVE_ORDER)
|
||||
.orElse("NONE"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
32
|
||||
a
|
||||
you
|
||||
```
|
||||
|
||||
`min()` 和 `max()` 的返回类型为 **Optional**,这需要我们使用 `orElse()`来解包。
|
||||
|
||||
### 数字流信息(Information for Numeric Streams)
|
||||
|
||||
- `average()` :求取流元素平均值。
|
||||
- `max()` 和 `min()`:因为这些操作在数字流上面,所以不需要 **Comparator**。
|
||||
- `sum()`:对所有流元素进行求和。
|
||||
- `summaryStatistics()`:生成可能有用的数据。目前还不太清楚他们为什么觉得有必要这样做, 但是你可以直接使用方法产生所有的数据。
|
||||
|
||||
```java
|
||||
// streams/NumericStreamInfo.java
|
||||
import java.util.stream.*;
|
||||
import static streams.RandInts.*;
|
||||
public class NumericStreamInfo {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(rands().average().getAsDouble());
|
||||
System.out.println(rands().max().getAsInt());
|
||||
System.out.println(rands().min().getAsInt());
|
||||
System.out.println(rands().sum());
|
||||
System.out.println(rands().summaryStatistics());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```java
|
||||
507.94
|
||||
998
|
||||
8
|
||||
50794
|
||||
IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998}
|
||||
```
|
||||
|
||||
这些操作对于 **LongStream** 和 **DoubleStream** 也同样适用。
|
||||
|
||||
## 本章小结
|
||||
|
||||
流改变了甚至极大的提升了 Java 编程的性质,并可能极大的组织了 Java 编程人员像诸如 scala 这种函数式语言的流动。在本书的剩余部分,我们将尽可能的使用流。
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user