0%

stream 流库

流提供了一种让我们可以在比集合更高的概念级别上指定计算的数据视图。

在处理集合时,我们通常会迭代遍历它的元素,并在每个元素上执行某项操作。例如,我们想要对某本书中的所有长单词进行计数。首先将所有单词放到一个列表中。

1
2
3
4
5
6
7
8
9
10
11
public long wordCount() throws IOException {
String contents;
contents = new String(Files.readAllBytes(Paths.get("alice.txt")), StandardCharsets.UTF_8);
List<String> words = Arrays.asList(contents.split("\\PL+"));

long count = 0;
for (String w : words) {
if (w.length() > 12) ++count;
}
return count;
}

使用流时,相同的操作看起来是下面这样:

1
long counts = words.stream().filter(w -> w.length() > 12).count();

流的版本比循环版本更易于阅读,因为我们不必扫描整个代码去查找过滤和计数操作,方法名就可以直接告诉我们其代码意欲何为。而且循环需要非常详细地指定操作的顺序,而流能够以其想要的任何方式来调度这些操作,只要结果时正确的即可。

流遵循了 “做什么而非怎么做” 的原则。

流和集合的差异:

  1. 流并不存储其元素,这些元素可能存储在底层的集合中,或者时按需生成的
  2. 流的操作不会修改其数据源。例如,filter方法不会从新的流中移除元素,而是会产生一个新的流,其中不包含被过滤掉的元素
  3. 流的操作时尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如,如果我们只想查找前 5 个长单词而不是所有的,那么filter方法就会在匹配到第 5 个单词后停止过滤。因此,我们甚至可以操作无限流。

在程序清单中,流是用 stream 或者 parallelStream 方法创建的。filter 方法对其进行转换,而 count 方法是终止操作。

1
2
3
4
5
6
// java.util.stram.Stream<T> 8
Stream<T> filter(Predicate<? super T> p) //产生一个流,其中半酣当前流中满足p的所有元素
long count() // 产生当前流中元素的数量,这是一个终止操作
// java.util.Collection<E> 8 产生当前集合中所有元素的顺序流或并行流
default Stream<E> stream()
default Stream<E> parallelStream()

创建流

java.util.stram.Streamstream 方法将任何集合转换为一个流。

如果你有一个数组,可以使用静态的 Stream.of 方法

1
Stream<String> words = Stream.of(contents.split("\\PL+"));

of 方法具有可变长参数,可以构建具有任意数量引元的流

1
Stream<String> song = Stream.of("gently", "down", "the", "stream");

创建无限流

Stream 接口有两个用于创建无限流的静态方法。

  1. 静态的 Stream.generate() 方法生成无限流,接受一个不包含引元的函数

    我们可以用如下方法获得一个常量值的流

    1
    Stream<String> echos = String.generate(() -> "echos");

    或者像下面一样获取一个随机数的流

    1
    String<Double> randoms = Stream.generate(Math::random);
  2. 静态的 Stream.iterate() 方法生成无限流,接受一个种子值以及一个函数,并且会反复地将函数应用到之前的结果上。

    为了产生无限序列,例如 0 1 2 3 ...,可以使用 iterate 方法

    1
    Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));

创建流部分 API

1
2
3
4
5
6
7
8
9
10
// 产生一个不包含任何元素的流
public static<T> Stream<T> empty()

// 产生一个元素为给定值的流
public static<T> Stream<T> of(T t)
public static<T> Stream<T> of(T... values)

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)

public static<T> Stream<T> generate(Supplier<T> s)

转换流

流的转换会产生一个新流,它的元素派生自另一个流中的元素。

filter转换会产生一个流,它的元素与某种条件相匹配。下面,我们将一个字符串流转换为只包含长单词的另一个流。

1
Stream<String> longWords = wordList.stream().filter(w -> w.length() > 12);

通常,我们想要按照某种方式来转换流中的值,此时,可以使用map方法并传递执行该转换的函数。例如,我们可以向下面这样将所有单词都转换为小写:

1
Stream<String> lowercaseWords = words.stream().map(String::tolowerCase);

这里,我们使用的是带有方法引用的 map,但是,通常我们可以使用 lambda 表达式来代替,下面语句产生的流包含了所有单词的首字母。

1
Stream<String> lowercaseWords = words.stream().map(s -> s.,substring(0, 1));

在使用 map 时,会有一个函数应用到每个元素上,并且其结果是包含了应用所函数后所产生的所有结果的流。

抽取子流和连接流

stream.limit(n)会返回一个新的流,它在n个元素之后结束(如果原来的流更短,那么就会在流结束时结束)。这个方法对于裁剪无限流的尺寸会显得特别有用。

1
Stream<Double> randoms = Stream.generate(Math::random).limit(100);

会产生一个包含100个随机数的流。

stream.skip(n) 方法会丢弃前n个元素。

1
Stream<String> words = Stream.of(contents.split("\\PL+")).skip(1);

Stream类的静态 concat() 方法将两个流连接起来

1
Stream<String> combined = Stream.concat(letters("hello"), letters("world"));

其他流的转换

distinct() 方法会返回一个流,原来的元素按照同样的顺序,剔除重复元素。

1
Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct();

sorted() 方法会产生一个新的流,它的元素时原有流中按照顺序排列的元素。对于流的排序,有很多 sorted 方法的变体可用。其中一种用于操作 Comparable 元素的流,而另一种可以接受一个 Comparator。下面的示例对字符串排序,使得最长的字符串排在最前面:

1
Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());

约简流

约简是一种终结操作,它们会将流约简为可以在程序中使用的非流值。

例如:count() 方法会返回流中元素的数量。max()min() 方法,返回最大值和最小值。 这些方法返回的是一个类型为 Optional 的值,它要么在其中包装了答案,要么标识没有任何值(因为流碰巧为空)。在过去,碰到这种情况返回 null 是很常见的,但是这样做会导致在未做晚辈测试的程序中产生空指针异常。Optional 类型是一种更好的标识缺少返回值的方法。下面展示了可以如何获得流中最大值:

1
2
Optional<String> largest = words.max(String::compareToIgnoreCase);
System.out.println("largest" + largest.orElse(""));

findFirst() 返回的是非空集合中的第一个值。它通常会在与 filter 组合使用时显得很有用。

1
Optional<String> startsWithQ = words.stream().filter(s -> s.startsWith("Q")).findFirst();

Optional类型

Optional 对象是一种包装器对象,要么包装了类型 T 对象,要么没有包装任何对象。对于第一种情况,我们称这种值为存在的。

有效地使用 Optioanal 的关键是要使用这样的方法:它在值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。

让我们爱看看第一条策略。通常,在没有任何匹配时,我们会希望使用某种默认值,可能时空字符串。

1
String result = optionalString.orElse("");

你还可以调用代码来计算默认值:

1
String result = optionalString.orElseGet(() -> Locale.getDefault().getDisplayName());

或者可以在没有任何值时抛出异常:

1
String result = optionalString.orElseThrow(IllegalStateException::new);

另一条使用可选值的策略是只有在其存在的情况下才消费该值。

ifPresent 方法接收一个函数,如果该可选值存在,那么它会被传递给该函数,否则,不会发生任何事。

1
optionalValue.ifpresent(v -> results.add());

或者直接调用

1
optionalValue.ifPresent(results::add);

当调用 ifPresent 时,从该函数不会返回任何值。如果想要处理函数的结果,应该使用 map:

1
Optional<Boolean> added = optionalValue.map(results::add);

现在 added 具有三种值之一:在 optionalValue 存在的情况下包装在 Optional 中的 true 或 false,以及在 optionalValue 不存在的情况下的空 Optional。

创建 Optional 值

有很多方法用来创建 Optional 对象,包括 Optional.of(result) 和 Optional.empty()。例如,

1
2
3
public static Optional<Double> inverse(Double x) {
return x == 0 ? Optional.empty() : Optional.of(1/x);
}

收集结果

当处理完流之后,通常会想要查看其元素,此时可以调用 iterator 方法, 它会产生可以用来访问元素的旧时风格的迭代器。

或者,可以调用 forEach 方法,将耨个函数应用于每个元素。

1
stream.foreach(System.out::println);

在并行流上, forEach 方法会以任意顺序遍历各个元素。如果想要按照流中的顺序来处理它们,可以调用 forEachOrdered 方法。当然,这个方法会丧失并行处理的部分甚至全部优势。

但是,更常见的是,我们想要将结果收集到数据结构中。此时,可以调用 toArray,获得由流的元素构成的数组。

因为无法在运行时创建泛型数组,所以表达式 stream.toArray() 会返回一个 Object[] 数据。如果想要让数组具有正确的类型,可以将其传递到数组构造器中:

1
String[] result = stream.toArray(String[]::new);

针对将流中的元素收集到另一个目标中,有一个边界方法 collect 可用,它会接受一个 Collector 接口的实例。Collectors 类提供了大量用于生成公共收集器的工厂方法。为了将流收集到列表或集中,可以直接调用

1
List<String> result = stream.collect(Collectors.toList());

1
Set<String> result = stream.collect(Collectors.toSet());

如果想要控制获得的集的种类,那么可以使用下面的调用

1
TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));

假设想要通过连接操作来手机流中的所有字符串,我们可以调用

1
String result = stream.collect(Collectors.joining());

如果想要在元素之间增加分隔符,可以将分隔符传递给 joining 方法:

1
String result = stream.collect(Collectors.joining(","));

如果流中包含除字符串以外的其他对象,那么我们需要现将其转换为字符串,就像下面这样:

1
String result = stream.map(Object::toString).collect(Collectors.joining(","));

收集到映射表中

假设我们有一个 Stream,并且想要将其元素收集到一个映射表中,这样后续就可以通过他们的 ID 来查找人员了。Collectors.toMap 方法有两个函数引元,它们用来产生映射表的键和值。例如:

1
Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));

并行流

流使得并行处理快操作变得很容易。这个过程几乎是自动的,但是需要遵守一些规则。首先,必须有一个并行流。可以用 Collection.parallelStream() 方法从任何集合中获取一个并行流:

1
Stream<String> parallelWords = words.parallelStream();

而且,parallel 方法可以将任意的顺序流转换为并行流。

1
Stream<String> parallelWords = Stream.of(wordArray).parallel();

只要在中介方法执行时,流处于并行模式,那么所有的中间流操作都将被并行化。

当流操作并行运行时,其目标是要让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作可以以任意顺序执行。