Java流(Stream、Optional)详解

  1. 1. Java8对流的支持
  2. 2. 流的创建
    1. 2.1 随机数流
    2. 2.2 整型的区间范围
    3. 2.3 Stream.generate()
    4. 2.4 Stream.iterate()
    5. 2.5 流生成器
    6. 2.6 Arrays
  3. 3. 中间操作
    1. 3.1 对流元素进行排序
    2. 3.2 移除元素
  4. 4. Optional类型
    1. 4.1 便捷函数
    2. 4.2 创建Optional
    3. 4.3 Optional对象上的操作
    4. 4.4 由Optional组成的流
  5. 5. 终结操作
    1. 5.1 将流转换为一个数组
    2. 5.2 在每个流元素上应用某个终结操作
    3. 5.3 收集操作
    4. 5.4 组合所有的流元素

本文介绍了 Java8 的一个十分强大的功能:流(Stream),能够优化传统实现方式中复杂冗余的代码。

1. Java8对流的支持

流是一个与任何特定的存储机制都没有关系的元素序列。不同于在集合中遍历元素,使用流的时候,我们是从一个管道中抽取元素,并对它们逬行操作。这些管道通常会被串联到一起,形成这个流上的一个操作管线。

流的一个核心优点是,它们能使我们的程序更小,也更好理解。当配合流使用时,Lambda 表达式和方法引用就发挥出其威力了。流大大提升了 Java 8 的吸引力。

假设我们想按照有序方式显示随机选择的5~20范围内的,不重复的整数,借助流我们只需要说明想做什么即可实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package stream;

import java.util.Random;

public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct().limit(5)
.sorted()
.forEach(System.out::println);

/*
* 6
* 10
* 13
* 16
* 18
*/
}
}

我们先为 Random 对象设置一个随机种子,ints() 方法会生成一个流,该方法有多个重载版本,其中两个参数的版本可以设置所生成值的上下界。使用中间流操作 distinct() 去掉重复的值,再使用 limit() 选择前5个值,sorted() 表示元素是有序的,最后我们想显示每一个条目,所以使用了 forEach(),它会根据我们传递的函数,在每个流对象上执行一个操作。这里我们传递了一个方法引用 System.out::println,用于将每个条目显示在控制台上。

使用流实现的代码我们看不到任何显式的迭代机制,因此称为内部迭代,这是流编程的一个核心特性。

2. 流的创建

使用 Stream.of(),可以轻松地将一组条目变成一个流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package stream;

import java.util.stream.Stream;

public class StreamOf {
public static void main(String[] args) {
Stream.of(4, 1, 7).forEach(System.out::println);
Stream.of("Hello ", "World ", "AsanoSaki").forEach(System.out::print);

/*
* 4
* 1
* 7
* Hello World AsanoSaki
*/
}
}

此外,每个 Collection 都可以使用 stream() 方法来生成一个流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package stream;

import java.util.*;
import java.util.stream.Collectors;

class A {
int x;

A(int x) { this.x = x; }
}

public class CollectionToStream {
public static void main(String[] args) {
List<A> listA = Arrays.asList(new A(1), new A(2), new A(3));
System.out.println(listA.stream().mapToInt(a -> a.x).sum());

Set<String> st = new TreeSet<>(Arrays.asList("Hello world and java".split(" ")));
System.out.println(st.stream().map(String::toUpperCase).collect(Collectors.joining(" ")));

Map<String, Double> mp = new HashMap<>();
mp.put("PI", 3.14);
mp.put("E", 2.718);
mp.put("PHI", 1.618);
mp.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue())
.forEach(System.out::println);

/*
* 6
* HELLO AND JAVA WORLD
* PHI: 1.618
* E: 2.718
* PI: 3.14
*/
}
}

在创建了一个 List<A> 之后,只需要调用一下 stream() 这个所有集合类都有的方法。中间的 map() 操作接受流中的毎个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去,这里的 mapToInt() 将一个对象流转变成了一个包含 IntegerIntStream。对于 FloatDouble 也有名字类似的操作。

collect() 操作会根据其参数将所有的流元素组合起来,当我们使用 Collectors.joining() 时,得到的结果是一个 String,每个元素都会以 joining() 的参数分隔,还有其他很多 Collectors,可以生成不同的结果。

为了从 Map 集合生成一个流,我们首先调用 entrySet() 来生成一个对象流,其中每个对象都包含着一个键和与其关联的值,然后再使用 getKey()getValue() 将其分开。

2.1 随机数流

Ramdom 类在 Java 8 引入了流,有一组可以生成流的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package stream;

import java.util.Random;
import java.util.stream.Stream;

public class RandomGenerators {
public static <T> void show(Stream<T> stream) {
stream.limit(3).map(x -> x + " ").forEach(System.out::print);
System.out.println();
}

public static void main(String[] args) {
Random rand = new Random(47);
show(rand.ints().boxed());
show(rand.doubles().boxed());
show(rand.ints(10, 20).boxed()); // 设置上下边界
show(rand.ints(2).boxed()); // 设置流的大小
show(rand.ints(2, 10, 20).boxed()); // 设置流的大小与上下边界

/*
* -1172028779 1717241110 -2014573909
* 0.053412216308810656 0.5779976127815049 0.4170137422770571
* 17 18 18
* 1122537102 491149179
* 19 18
*/
}
}

为消除冗余代码,上面的示例创建了泛型方法 show(Stream<T> stream),这个特性在之后会讲。类型参数 T 可以是任何东西,所以使用 IntegerLongDouble 都可以。然而,Random 类只会生成 intlongdouble 等基本类型的值。幸运的是,boxed() 流操作会自动将基本类型转换为其对应的包装器类型,使得 show() 能够接受这个流。

2.2 整型的区间范围

IntStream 类提供了一个 range() 方法,可以生成一个流(由 int 值组成的序列),在编写循环时非常方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package stream;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class IntStreamRange {
public static void main(String[] args) {
// for-in循环
for (int i: IntStream.range(1, 5).toArray())
System.out.print(i + " "); // 1 2 3 4
System.out.println();

// Stream
System.out.println(IntStream.range(1, 5).boxed().map(Object::toString).collect(Collectors.joining(" "))); // 1 2 3 4
IntStream.range(1, 5).boxed().forEach(x -> System.out.print(x + " ")); // 1 2 3 4
System.out.println();
}
}

现在我们编写一个 repeat() 工具函数取代简单的 for 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package stream;

import java.util.stream.IntStream;

public class Repeat {
static void repeat(int n, Runnable action) {
IntStream.range(0, n).forEach(i -> action.run());
}

static void hello() {
System.out.println("Hello");
}

public static void main(String[] args) {
repeat(3, () -> System.out.println("Lambda"));
repeat(2, Repeat::hello);

/*
* Lambda
* Lambda
* Lambda
* Hello
* Hello
*/
}
}

2.3 Stream.generate()

Stream.generate() 可以接受任何的 Supplier<T>java.util.function 中的接口),并生成一个由 T 类型的对象组成的流。如果想创建一个由完全相同的对象组成的流,只需要将一个生成这些对象的 Lambda 表达式传给 generate()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package stream;

import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Generator implements Supplier<String> {
Random rand = new Random(47);
char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

@Override
public String get() {
return String.valueOf(letters[rand.nextInt(letters.length)]);
}

public static void main(String[] args) {
String str = Stream.generate(new Generator()).limit(10).collect(Collectors.joining());
System.out.println(str); // YNZBRNYGCF

Stream.generate(() -> "AsanoSaki").limit(2).forEach(s -> System.out.print(s + " ")); // AsanoSaki AsanoSaki
}
}

2.4 Stream.iterate()

Stream.iterate() 从一个种子开始(第一个参数),然后将其传给第二个参数所引用的方法,其结果被添加到这个流上,并且保存下来作为下一次 iterate() 调用的第一个参数,以此类推。我们可以通过迭代生成一个之前实现过的斐波那契数列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package stream;

import java.util.stream.Stream;

public class StreamFibonacci {
int x = 1;

Stream<Integer> fib() {
return Stream.iterate(0, i -> {
int res = x + i;
x = i;
return res;
});
}

public static void main(String[] args) {
new StreamFibonacci().fib().limit(10).forEach(x -> System.out.print(x + " "));
System.out.println();
new StreamFibonacci().fib().skip(10).limit(10).forEach(x -> System.out.print(x + " "));

/*
* 0 1 1 2 3 5 8 13 21 34
* 55 89 144 233 377 610 987 1597 2584 4181
*/
}
}

iterate() 只会记住结果,所以必须使用 x 来记住另一个元素。我们使用了 skip() 操作,这个之前没有介绍过,它会直接丢弃由其参数指定的相应数目的流元素,这里丢弃了前10个。

2.5 流生成器

生成器(Builder)设计模式中,我们创建一个生成器对象,为它提供多段构造信息,最后执行生成(build)动作。 Stream 库提供了这样一个 Builder,假设我们实现读取文件的每一行并将其转换为单词流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package stream;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class FileToWordsBuilder {
Stream.Builder<String> builder = Stream.builder();

public FileToWordsBuilder(String filePath) throws Exception {
Files.lines(Paths.get(filePath)).forEach(line -> {
for (String s: line.split("[ .?,!]+"))
builder.add(s);
});
}

public static void main(String[] args) throws Exception {
String filePath = "src/file/FileToWordsBuilder.txt";
new FileToWordsBuilder(filePath).builder.build()
.forEach(s -> System.out.print(s + " "));

/*
* Hello world Today is Saturday Have a nice day
*/
}
}

其中的文本内容如下:

1
2
3
Hello world!
Today is Saturday?
Have a nice day!

构造器添加了文件中的所有单词,但是它没有调用 build(),这意味着只要不调用 build() 就可以继续向 Builder 对象中添加单词。如果在调用 build() 之后还尝试向 Stream.Builder 中添加单词,则会产生异常。

2.6 Arrays

Arrays 类中包含了名为 stream() 的静态方法,可以将数组转换为流。可以是任何对象数组,也可以是 intlongdouble 基本类型,生成 IntStreamLongStreamDoubleStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package stream;

import java.util.Arrays;

public class ArraysStream {
public static void main(String[] args) {
Arrays.stream(new String[] { "ABC", "XYZ", "YYJ" })
.forEach(s -> System.out.print(s + " "));
System.out.println();

Arrays.stream(new int[] { 1, 2, 3 })
.forEach(x -> System.out.printf("%d ", x));
System.out.println();

Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })
.forEach(x -> System.out.printf("%.2f ", x));
System.out.println();

Arrays.stream(new int[] { 9, 8, 7, 6, 5 }, 1, 4)
.forEach(x -> System.out.printf("%d ", x));
System.out.println();

/*
* ABC XYZ YYJ
* 1 2 3
* 3.14 2.72 1.62
* 8 7 6
*/
}
}

最后一次调用 stream() 时使用了两个额外的参数,第一个表示从数组的哪个位置开始选择元素,第二个表示停止位置(开区间),即在本例中选择数组中 [1, 4) 的元素。

3. 中间操作

3.1 对流元素进行排序

使用 sorted() 可以进行默认排序(从小到大),该方法也可以接受一个 Comparator 参数,该参数可以传入一个 Lambda 表达式也可以使用预定义好的 Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package stream;

import java.util.Comparator;
import java.util.stream.Stream;

public class StreamSorted {
public static void main(String[] args) {
Stream.of(1, 2, 3)
.sorted(Comparator.reverseOrder())
.forEach(x -> System.out.print(x + " "));

/*
* 3 2 1
*/
}
}

3.2 移除元素

  • distinct():移除流中的重复元素。
  • filter(Predicate):过滤只保留符合特定条件的元素,即满足过滤函数 Predicatetrue 的流元素。

来看一个筛选素数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package stream;

import java.util.stream.LongStream;

public class Prime {
static boolean isPrime(long n) {
return LongStream.rangeClosed(2, (long)Math.sqrt(n)).noneMatch(i -> n % i == 0);
}

static LongStream getPrime() {
return LongStream.iterate(2, i -> i + 1).filter(Prime::isPrime);
}

public static void main(String[] args) {
Prime.getPrime().limit(10).forEach(x -> System.out.printf("%d ", x));
System.out.println();

/*
* 2 3 5 7 11 13 17 19 23 29
*/
}
}

rangeClosed() 包含了上界值,如果流中的元素没有任何一个取余操作的结果为0,则 noneMatch() 操作返回 true,如果有任何一个计算结果等于0,则返回 falsenoneMatch() 会在第一次失败后退出,而不会把后面的所有计算都尝试一遍。

4. Optional类型

在研究终结操作之前,我们必须考虑一个问题:如果我们向流中请求对象,但是流中什么都没有,这时会发生什么呢?有没有某种我们可以使用的对象,既可以作为流元素来占位,也可以在我们要找的元素不存在时友好地告知我们(也就是说,不会抛出异常)。

这个想法被实现为 Optional 类型,某些标准的流操作会返回 Optional 对象,因为它们不能确保所要的结果一定存在,这些流操作列举如下:

  • findFirst():返回包含第一个元素的 Optional。如果这个流为空,则返回 Optional.empty
  • findAny():返回包含任何元素的 Optional。如果这个流为空,则返回 Optional.empty
  • max()min() 分别返回包含流中最大值或最小值的 Optional,如果这个流为空,则返回 Optional.empty
  • reduce() 的一个版本,它并不以一个 identity 对象作为其第一个参数(在 reduce() 的其他版本中,identity 对象会成为默认结果,所以不会有结果为空的风险),它会将返回值包在一个 Optional 中。
  • 对于数值化的流 IntStreamLongStreamDoubleStreamaverage() 操作将其结果包在一个 Optional 中,以防流为空的情况。

看一下以下代码样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package stream;

import java.util.Optional;
import java.util.stream.Stream;

public class StreamOptional {
static void test(Optional<String> stringOptional) {
if (stringOptional.isPresent())
System.out.println(stringOptional.get());
else
System.out.println("Nothing in Optional!");
}

public static void main(String[] args) {
System.out.println(Stream.<String>empty().findFirst());
test(Stream.of("Hello").findFirst());
test(Stream.<String>empty().findFirst());

/*
* Optional.empty
* Hello
* Nothing in Optional!
*/
}
}

这时不会因为流是空的而抛出异常,而是会得到一个 Optional.empty 对象。Optional 有一个 toString() 方法,可以显示有用的信息。

注意,空流是通过 Stream.<String>empty() 创建的,如果只用了 Stream.empty() 而没有任何上下文信息,那么 Java 不知道它应该是什么类型的,而这种语法解决了该问题。如果编译器有足够的上下文信息,那么它可以推断出 empty() 调用的类型,就像下面这样:

1
Stream<String> s = Stream.empty();

我们接收到一个 Optional 时,首先要调用 isPresent(),看看里面是不是有东西,如果有,再使用 get() 来获取。

4.1 便捷函数

有很多便捷函数,可用于获取 Optional 中的数据,它们简化了上面先检查再处理所包含对象的过程:

  • ifPresent(Consumer):如果对象存在,则用这个对象来调用 Consumer,否则什么都不做。
  • orElse(otherObject):如果对象存在,则返回这个对象,否则返回 otherObject
  • orElseGet(Supplier):如果对象存在,则返回这个对象,否则返回使用 Supplier 函数创建的替代对象
  • orElseThrow(Supplier):如果对象存在,则返回这个对象,否则抛出一个使用 Supplier 函数创建的异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package stream;

import java.util.Optional;
import java.util.stream.Stream;

public class OptionalFunctions {
static Optional<String> emptyOptional = Stream.<String>empty().findFirst();
static Optional<String> stringOptional = Stream.of("Hello").findFirst();

public static void main(String[] args) {
System.out.println("---------- ifPresent ----------");
emptyOptional.ifPresent(System.out::println);
stringOptional.ifPresent(System.out::println);

System.out.println("---------- orElse ----------");
System.out.println(emptyOptional.orElse("Other String"));
System.out.println(stringOptional.orElse("Other String"));

System.out.println("---------- orElseGet ----------");
System.out.println(emptyOptional.orElseGet(() -> "Other Supplier Object"));
System.out.println(stringOptional.orElseGet(() -> "Other Supplier Object"));

System.out.println("---------- orElseThrow ----------");
try {
System.out.println(stringOptional.orElseThrow(() -> new Exception("Supplier Exception"))); // 先执行非空Optional
System.out.println(emptyOptional.orElseThrow(() -> new Exception("Supplier Exception")));
} catch (Exception e) {
System.out.println("Caught" + e);
}

/*
* ---------- ifPresent ----------
* Hello
* ---------- orElse ----------
* Other String
* Hello
* ---------- orElseGet ----------
* Other Supplier Object
* Hello
* ---------- orElseThrow ----------
* Hello
* Caughtjava.lang.Exception: Supplier Exception
*/
}
}

4.2 创建Optional

当需要自己编写生成 Optional 的代码时,有如下三种可以使用的静态方法:

  • empty():返回一个空的 Optional
  • of(value):如果已经知道这个 value 不为 null,可以使用该方法将其包在一个 Optional 中。
  • ofNullable(value):如果不知道这个 value 是不是 null,可以使用这个方法,如果 valuenull,它会自动返回 Optional.empty,否则会将这个 value 包在一个 Optional 中。

来看一下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package stream;

import java.util.Optional;

public class CreatingOptionals {
static Optional<String>
stringOptional = Optional.of("Hello"),
emptyOptional = Optional.empty(),
nullOptional = Optional.ofNullable(null);

public static void main(String[] args) {
System.out.println(stringOptional.orElse("Empty"));
System.out.println(emptyOptional.orElse("Empty"));
System.out.println(nullOptional.orElse("Empty"));
}

/*
* Hello
* Empty
* Empty
*/
}

4.3 Optional对象上的操作

有三种方法支持对 Optional 进行事后处理,所以如果你的流管线生成了一个 Optional,你可以在最后再做一项处理:

  • filter(Predicate):将 Predicate 应用于 Optional 的内容,并返回其结果。如果 OptionalPredicate 不匹配,则将其转换为 empty。如果 Optional 本身已经是 empty,则直接传回。
  • map(Function):如果 Optional 不为 empty,则将 Function 应用于 Optional 中包含的对象,并返回结果,否则传回 Optional.empty
  • flatMap(Function):和 map() 类似,但是所提供的映射函数会将结果包在 Optional 中,这样 flatMap() 最后就不会再做任何包装了。

数值化的 Optional 上没有提供这些操作。

我们来看一下 filter 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package stream;

import java.util.Arrays;

public class OptionalFilter {
static String[] elements = { "Dog", "Cat", "", "Bird" };

public static void main(String[] args) {
for (int i = 0; i <= elements.length; i++)
System.out.println(
Arrays.stream(elements)
.skip(i)
.findFirst()
.filter(s -> s.length() == 3)
);

/*
* Optional[Dog]
* Optional[Cat]
* Optional.empty
* Optional.empty
* Optional.empty
*/
}
}

尽管输出看上去像是一个流,其实每次进入 for 循环,它都会重新获得一个流,并跳过用 for 循环的索引设置的元素数,这就使其看上去像流中的连续元素,然后它执行 findFirst(),获得剩余元素的中的第一个,它会被包在一个 Optional 中返回。

注意,我们的 for 循环是循环到 i == elements.length,因此最后一个元素会超出这个流。不过这会自动变为 Optional.empty

4.4 由Optional组成的流

假设有一个可能会生成 null 值的生成器,如果使用这个生成器创建了一个流,我们自然想将这些元素包在 Optional 中,当使用这个流时,我们必须弄清楚如何获得 Optional 中的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package stream;

import java.util.Optional;
import java.util.Random;
import java.util.stream.Stream;

class Signal {
private final String msg;
static Random rand = new Random(47);

Signal(String msg) { this.msg = msg; }

@Override
public String toString() {
return "Signal(" + msg + ")";
}

public static Signal getSignal() {
switch (rand.nextInt(4)) {
case 1: return new Signal("Case 1");
case 2: return new Signal("Case 2");
default: return null;
}
}

public static Stream<Optional<Signal>> getSignalOptStream() {
return Stream.generate(Signal::getSignal).map(Optional::ofNullable);
}
}

public class StreamOfOptionals {
public static void main(String[] args) {
Signal.getSignalOptStream().limit(5).forEach(System.out::println);
System.out.println("--------------------");
Signal.getSignalOptStream().limit(5)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(System.out::println);

/*
* Optional[Signal(Case 2)]
* Optional[Signal(Case 1)]
* Optional[Signal(Case 2)]
* Optional.empty
* Optional.empty
* --------------------
* Signal(Case 2)
* Signal(Case 1)
* Signal(Case 2)
* Signal(Case 2)
*/
}
}

这里我使用了 只保留非 emptyOptional,然后通过 map() 调用 get() 来获得包在其中的对象,因为每种情况都需要我们来决定“没有值”的含义,所以我们通常需要针对每种应用采取不同的方法。

5. 终结操作

这些操作接受一个流,并生成一个最终结果,它们不会再把任何东西发给某个后端的流。因此,终结操作总是我们在一个管线内可以做的最后一件事。

5.1 将流转换为一个数组

  • toArray():将流元素转换到适当类型的数组中。
  • toArray(generator)generator 用于在特定情况下分配自己的数组存储。

直接看样例:

1
2
3
4
5
6
7
8
9
10
11
package stream;

import java.util.Arrays;
import java.util.Random;

public class RandInts {
public static void main(String[] args) {
int[] nums = new Random(47).ints(0, 10).limit(10).toArray();
System.out.println(Arrays.toString(nums)); // [8, 5, 3, 1, 1, 9, 8, 0, 2, 7]
}
}

5.2 在每个流元素上应用某个终结操作

  • forEach(Consumer):这种用法我们已经看到过很多次了,即以 System.out::println 作为 Consumer 函数。
  • forEachOrdered(Consumer):这个版本确保 forEach 对元素的操作顺序是原始的流的顺序。

第一种形式被明确地设计为可以以任何顺序操作元素,这只有在引入 parallel() 操作时才有意义。parallel() 让 Java 尝试在多个处理器上执行操作。它可以做到这一点,正是因为使用了流,它可以将流分割为多个流(通常情况是每个处理器一个流),并在不同的处理器上运行每个流。

我们在以下示例中引入 parallel() 来了解 forEachOrdered() 的作用和必要性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package stream;

import java.util.Arrays;
import java.util.Random;

public class ForEachOrdered {
public static void main(String[] args) {
int[] nums = new Random(47).ints(0, 10).limit(10).toArray(); // 确保每次流元素都相同

Arrays.stream(nums).forEach(x -> System.out.printf("%d ", x));
System.out.println();

Arrays.stream(nums).parallel().forEach(x -> System.out.printf("%d ", x));
System.out.println();

Arrays.stream(nums).parallel().forEachOrdered(x -> System.out.printf("%d ", x));
System.out.println();

/*
* 8 5 3 1 1 9 8 0 2 7
* 8 9 0 7 2 3 5 1 8 1
* 8 5 3 1 1 9 8 0 2 7
*/
}
}

在第一个流中,我们没有使用 parallel(),所以结果的显示顺序就是它们从 Arrays.stream(nums) 中出现的顺序。第二个流引入了 parallel(),即便是这么小的一个流,我们也可以看到输出的顺序和之前不一样了。这是因为有多个处理器在处理这个问题,而且如果多次运行这个程序,会发现每次的输出还会有所不同,原因在于多个处理器同时处理这个问题所带来的不确定性因素。

最后一个流仍然使用了 parallel(),但是又使用 forEachOrdered() 来强制结果回到原始的顺序。因此,对于非 parallel() 的流,使用 forEachOrdered() 不会有任何影响。

5.3 收集操作

  • collect(Collector):使用这个 Collector 将流元素累加到一个结果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):和上面类似,但是 Supplier 会创建一个新的结果集合,第一个 BiConsumer 是用来将下一个元素包含到结果中的函数,第二个 BiConsumer 用于将两个值组合起来。

我们之前仅仅看到了 Collectors 对象的几个示例,我们可以将流元素收集到任何特定种类的集合中。假设想把我们的条目最终放到一个 TreeSet 中,由此使它们总是有序的。在 Collectors 中没有特定的 toTreeSet() 方法,只有 toSet(),但是可以使用 Collectors.toCollection(),并将任何类型的 Collection 的构造器引用传给它。下面的程序提取文件中的单词放到 TreeSet 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package stream;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

public class TreeSetOfWords {
public static void main(String[] args) throws Exception {
String filePath = "src/file/FileToWordsBuilder.txt";
Set<String> words = Files.lines(Paths.get(filePath))
.flatMap(line -> Arrays.stream(line.split("\\W+")))
.filter(word -> !word.matches("\\d+"))
.map(String::trim)
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(words);

/*
* [Have, Hello, Saturday, This, Today, a, day, digit, is, nice, world]
*/
}
}

其中 FileToWordsBuilder.txt 文件内容如下:

1
2
3
4
Hello world!
Today is Saturday?
Have a nice day!
This is digit 666.

Files.lines() 打开 Path 所指向的文件,并将其变为由文本行组成的 Stream。它的下一行代码以一个或多个非单词字符(\\W+)为边界来分割这些文本行,这里生成的数组通过 Arrays.stream() 变为 Stream,然后其结果又被展开映射回一个由单词组成的 Streammatches(\\d+) 会找到并删除全是数字的 String。接下来使用 String.trim() 去除周围可能存在的任何空白,最后把这些单词放到一个 TreeSet 中。

5.4 组合所有的流元素

  • reduce(BinaryOperator):使用 BinaryOperator 来组合所有的流元素,因为这个流可能为空,所以返回的是一个 Optional
  • reduce(identity, BinaryOperator):和上面一样,但是将 identity 用作这个组合的初始值,因此,即使这个流是空的,我们仍然能得到 identity 作为结果。
  • reduce(identity, BiFunction, BinaryOperator):这个更复杂(所以我们不会介绍),但是之所以把它列在这里,是因为它可能更高效。可以通过组合显式的 map()reduce() 操作来更简单地表达这种需求。

来看一下最简单的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package stream;

import java.util.Random;
import java.util.stream.Stream;

class Apple {
private final int price;

Apple(int price) { this.price = price; }

int getPrice() { return price; }

@Override
public String toString() {
return "Apple(" + price + ")";
}

// 生成器
static Random rand = new Random(47);
static Apple generator() {
return new Apple(rand.nextInt(100));
}
}

public class Reduce {
public static void main(String[] args) {
Stream.generate(Apple::generator)
.limit(5)
.peek(a -> System.out.println("Peek: " + a))
.reduce((a0, a1) -> a0.getPrice() > 60 ? a0 : a1)
.ifPresent(System.out::println);

/*
* Peek: Apple(58)
* Peek: Apple(55)
* Peek: Apple(93)
* Peek: Apple(61)
* Peek: Apple(61)
* Apple(93)
*/
}
}

我们在使用 reduce() 时,没有提供作为初始值的第一个参数,这意味着它会生成一个 Optional,只有当结果不是 empty 时,Optional.ifPresent() 方法才会调用 Consumer<Apple>(之所以 System.out::println 能够符合,是因为它可以通过 toString() 方法将 Apple 转化为一个 String)。

Lambda 表达式中的第一个参数 a0 是上次调用这个 reduce() 时带回的结果,第二个参数 a1 是来自流中的新值。如果 a0price 大于60就接受 a0,否则就接受 a1,也就是序列中的下一个元素。

作为结果,我们得到的是流中第一个 price 大于60的 Apple,一旦找到了一个这样的对象,它就会抓住不放,哪怕还会出现其他候选。