20220424 Java核心技术 卷2 高级特性 1

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();

流遵循了 【做什么而非怎么做】的原则。

流表面上看起来和集合很类似,都可以让我们转换和获取数据。但是,它们之间存在着显著的差异:

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

让我们再来看看这个示例。 streamparallelStream 方法会产生一个用于 words 列表的 stream 。 filter 方法会返回另一个流,其中只包含长度大于 12 的单词。 count 方法会将这个流化简为一个结果。

这个工作流是操作流时的典型流程。我们建立了一个包含三个阶段的操作管道

  1. 创建 一个流
  2. 指定将初始流 转换 为其他流的中间操作,可能包含多个步骤
  3. 应用 终止 操作,从而产生结果。这个操作会强制执行之前的惰性操作。从此之后,这个流就再也不能用了

上面的示例,流是用 streamparallelStream 方法创建, filter 方法转换,count 方法终止。

java.util.stream.Stream<T> 方法名称 方法声明 描述
filter Stream<T> filter(Predicate<? super T> predicate); 产生一个流,其中包含当前流中满足 predicate 的所有元素
count long count(); 产生当前流中元素的数量。这是一个终止操作
java.util.Collection<E> 方法名称 方法声明 描述
stream
parallelStream
default Stream<E> stream()
default Stream<E> parallelStream()
产生当前集合中所有元素的顺序流或并行流

流的创建

可以用 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);
}
java.util.stream.Stream<T> 方法名称 方法声明 描述
of public static<T> Stream<T> of(T... values) 产生一个元素为给定值的流
empty public static<T> Stream<T> empty() 产生一个不包含任何元素的流
generate public static<T> Stream<T> generate(Supplier<T> s) 产生一个无限流,它的值是通过反复调用函数 s 而构建的
iterate public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) 产生一个无限流,它的元素包含种子、在种子上调用 f 产生的值、在前一个元素上调用 f 产生的值,等等
java.util.Arrays 方法名称 方法声明 描述
stream public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) 产生一个流,它的元素是由数组中指定范围内的元素构成的
java.util.regex.Pattern 方法名称 方法声明 描述
splitAsStream public Stream<String> splitAsStream(final CharSequence input) 产生一个流,它的元素是输入中由该模式界定的部分
java.nio.file.Files 方法名称 方法声明 描述
lines public static Stream<String> lines(Path path) throws IOException
public static Stream<String> lines(Path path, Charset cs) throws IOException
产生一个流,它的元素是指定文件中的行,该文件的字符集为 UTF-8 ,或者为指定的字符集
java.util.function.Supplier<T> 方法名称 方法声明 描述
get T get(); 提供一个值

filtermapflatMap 方法

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

filter 转换会产生一个流,它的元素与某种条件相匹配。filter 的引元是 Predicate<T>,即从 Tboolean 的函数

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

java.util.stream.Stream<T> 方法名称 方法声明 描述
filter Stream<T> filter(Predicate<? super T> predicate); 产生一个流,它包含当前流中所有满足断言条件的元素
map <R> Stream<R> map(Function<? super T, ? extends R> mapper); 产生一个流, 它包含将 mapper 应用于当前流中所有元素所产生的结果
flatMap <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); 产生一个流,它是通过将 mapper 应用于当前流中所有元素所产生的结果连接到一起而获得的(注意,这里的每个结果都是一个流)

抽取子流和连接流

调用 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"));
java.util.stream.Stream<T> 方法名称 方法声明 描述
limit Stream<T> limit(long maxSize); 产生一个流,其 包含了当前流中最初的 maxSize 个元素
skip Stream<T> skip(long n); 产生一个流,它的元素是当前流中除了前 n 个元素之外的所有元素
concat public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) 产生一个流,它的元素是 a 的元素后面跟着 b 的元素

其他的流转换

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 调用一个你设置了断点的方法

java.util.stream.Stream<T> 方法名称 方法声明 描述
distinct Stream<T> distinct(); 产生一个流,包含当前流中所有不同的元素
sorted Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
产生一个流,它的元素是当前流中的所有元素按照顺序排列的。第一个方法要求元素是实现了 Comparable 的类的实例
peek Stream<T> peek(Consumer<? super T> action); 产生一个流,它与当前流中的元素相同,在获取其中每个元素时,会将其传递给 action

简单约简

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

count 方法会返回流中元素的数量,maxmin ,会返回最大值和最小值,这些方法返回的是一个类型 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);

还有 allMatchnoneMatch 方法,它们分别会在所有元素和没有任何元素匹配断言的情况下返回 true 。这些方法也可以通过并行运行而获益

java.util.stream.Stream<T> 方法名称 方法声明 描述
max
min
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
分别产生这个流的最大元素和最小元素,使用由给定比较器定义的排序规则,如果这个流为空,会产生一个空的 Optional 对象 这些操作都是终结操作
findFirst
findAny
Optional<T> findFirst();
Optional<T> findAny();
分别产生这个流的第一个和任意一个元素,如果这个流为空,会产生一个空的 Optional 对象。这些操作都是终结操作
anyMatch
allMatch
noneMatch
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
分别在这个流中任意元素、所有元素和没有任何元素匹配给定断言时返回 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,并且在后一种情况中,会应用到函数

java.util.Optional<T> 方法名称 方法声明 描述
orElse public T orElse(T other) 产生这个 Optional 的值,或者在该 Optional 为空时,产生 other
orElseGet public T orElseGet(Supplier<? extends T> other) 产生这个 Optional 的值,或者在该 Optional 为空时,产生调用 other 的结果
orElseThrow public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X 产生这个 Optional 的值,或者在该 Optional 为空时,抛出调用 exceptionSupplier 的结果
ifPresent public void ifPresent(Consumer<? super T> consumer) 如果该 Optional 不为空,那么就将它的值传递给 consumer
map public<U> Optional<U> map(Function<? super T, ? extends U> mapper) 产生将该 Optional 的值传递给 mapper 后的结果,只要这个 Optional 不为空且结果不为 null ,否则产生一个空 Optional

不适合使用 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();
}
java.util.Optional<T> 方法名称 方法声明 描述
get public T get() 产生这个 Optional 的值,或者在该 Optional 为空时,抛出 NoSuchElementException 异常
isPresent public boolean isPresent() 如果该 Optional 不为空,则返回 true

创建 Optional

ofNullable 方法被用来作为可能出现的 null 值和可选值之间的桥梁。Optional.ofNullable(obj) 会在 obj 不为 null 的情况下返回 Optional.of(obj) ,否则会返回 Optional.empty()

java.util.Optional<T> 方法名称 方法声明 描述
of
ofNullable
public static <T> Optional<T> of(T value)
public static <T> Optional<T> ofNullable(T value)
产生一个具有给定值的 Optional 。如果 valuenull ,那么第一个方法会抛出 NullPointerException 对象,而第二个方法会产生一个空 Optional
empty public static<T> Optional<T> empty() 产生一个空 Optional

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 方法与其操作方式一样

java.util.Optional<T> 方法名称 方法声明 描述
flatMap public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) 产生将 mapper 应用于当前的 Optional 值所产生的结果,或者在当前 Optional 为空时,返回一个空 Optional

收集结果( 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(","));

如果想要将流的结果约简为总和、平均值、最大值或最小值,可以使 summarizingInt | 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();
java.util.stream.BaseStream<T, S extends BaseStream<T, S>> 方法名称 方法声明 描述
iterator Iterator<T> iterator(); 产生一个用于获取当前流中各个元素的迭代器。 这是 终结操作
java.util.stream.Stream<T> 方法名称 方法声明 描述
forEach void forEach(Consumer<? super T> action); 在流的每个元素上调用 action 。这是一种 终结操作
toArray Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);
产生一个对象数组,或者在将引用 A[]::new 传递给构造器时,返回一个 A 类型的数组。这些操作都是 终结操作
collect <R, A> R collect(Collector<? super T, A, R> collector); 使用给定的收集器来收集当前流中的元素。 Collectors 类有用于多种收集器的工厂方法
java.util.stream.Collectors 方法名称 方法声明 描述
toList
toSet
public static <T> collector<T, ?, List<T>> toList()
public static <T> Collector<T, ?, Set<T>> toSet()
产生一个将元素收集到列表或集中的收集器
toCollection public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) 产生一个将元素收集到任意集合中的收集器。可以传递一个诸如 TreeSet::new 的构造器引用
joining public static Collector<CharSequence, ?, String> joining()
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
产生一个连接字符串的收集器。分隔符会置于字符串之间,而第一个字符串之前可以有前缀,最后一个字符串之后可以有后缀。如果没有指定,那么它们都为空
summarizingInt
summarizingLong
summarizingDouble
public static <T> Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
public static <T> Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
public static <T> Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
产生能够生成 ( Int
java.util.IntSummaryStatistics 方法名称 方法声明 描述
getCount public final long getCount() 产生汇总后的元素的个数
getSum
getAverage
public final long getSum()
public final double getAverage()
产生汇总后的元素的总和或平均值,或者在没有 何元素时返回 0
getMax
getMin
public final int getMax()
public final int getMin()
产生汇总后的元素的最大值和最小值,或者在没有任何元素时,产生 Integer.MAX_VALUEInteger.MIN_VALUE

java.util.LongSummaryStatisticsjava.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 方法,单个并发映射表可以用于并行集合处理。当使用并行流时,共享的映射表比合并映射表要更高效。注意,元素不再是按照流中的顺序收集的,但是通常这不会有什么问题

java.util.stream.Collectors 方法名称 方法声明 描述
toMap
toConcurrentMap
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
public static <T, K, U> Collector<T, ?, ConcurrentMap<K,U>> toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)
public static <T, K, U> Collector<T, ?, ConcurrentMap<K,U>> toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
public static <T, K, U, M extends ConcurrentMap<K, U>> Collector<T, ?, M> toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
产生一个收集器,它会产生一个映射表或并发映射表。keyMappervalueMapper 函数会应用于每个收集到的元素上,从而在所产生的映射表中生成一个键/值项。默认情况下,当两个元素产生相同的键时,会抛出一个 IllegalStateException 异常。你可以提供一个 mergeFunction 来合并具有相同键的值。默认情况下,其结果是 HashMapConcurrentHashMap 。你可以提供 mapSupplier ,它会产生所期望的映射表实例

群组和分区( groupingBypartitioningBy

每个 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));
java.util.stream.Collectors 方法名称 方法声明 描述
groupingBy
groupingByConcurrent
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
public static <T, K> Collector<T, ?, ConcurrentMap<K, List<T>>> groupingByConcurrent(Function<? super T, ? extends K> classifier)
产生一个收集器,它会产生一个映射表或并发映射表,其键是将 classifier 应用于所有收集到的元素上所产生的结果,而值是由具有相同键的元素构成的一个个列表
partitioningBy public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) 产生一个收集器,它会产生一个映射表,其键是 true / false ,而值是由满足 / 不满足断言的元素构成的列表

下游收集器

groupingBy 方法会产生一个映射表,它的每个值都是一个 List 。如果想要以某种方式来处理这些列表,就需要提供一个“下游收集器”

获得 Set 而不是 List

Map<String, Set<Locale>> countryToLocaleSet = locales.collect(Collectors.groupingBy(
    Locale::getCountry, Collectors.toSet()));

Java 提供了多种可以将群组元素约简为数字的收集器:

  • counting 会产生收集到的元素的个数
  • summing ( Int | Long | Double ) 会接受一个函数作为引元,将该函数应用到下游元素中,并产生它们的和
  • maxByminBy 会接受一个比较器,并产生下游元素中的最大值和最小值
  • 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)));

将收集器组合起来是一种很强大的方式,但是它也可能会导致产生非常复杂的表达式。它们的最佳用法是与 groupingBypartitioningBy 一起处理 “下游的” 映射表中的值。否则,应该直接在流上应用诸如 mapreducecountmaxmin 这样的方法

java.util.stream.Collectors 方法名称 方法声明 描述
counting public static <T> Collector<T, ?, Long> counting() 产生一个可以对收集到的元素进行计数的收集器
summingInt
summingLong
summingDouble
public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper)
public static <T> Collector<T, ?, Long> summingLong(ToLongFunction<? super T> mapper)
public static <T> Collector<T, ?, Double> summingDouble(ToDoubleFunction<? super T> mapper)
产生一个收集器,对将 mapper 应用到收集到的元素上之后产生的值计算总和
maxBy
minBy
public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator)
public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator)
产生一个收集器,使用 comparator 指定的排序方法,计算收集到的元素中的最大值和最小值
mapping public static <T, U, A, R> Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper, Collector<? super U, A, R> downstream) 产生一个收集器,它会产生一个映射表,其键是将 mapper 应用到收集到的数据上而产生的,其值是使用 downstream 收集器收集到的具有相同键的元素

约简操作( 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 ,它会接受单个引元:

  1. 一个提供者,它会创建目标类型的新实例,例如散列集的构造器
  2. 一个累积器,它会将一个元素添加到一个实例上,例如 add 方法
  3. 一个组合器,它会将两个实例合并成一个,例如 addAll
BitSet result = words.stream().collect(BitSet::new, BitSet::or, BitSet::or);
java.util.stream.Collectors 方法名称 方法声明 描述
reduce Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
用给定的 accumulator 函数产生流中元素的累积总和。如果提供了幺元,那么第一个被累计的元素就是该么元。如果提供了组合器,那么它可以用来将分别累积的各个部分整合成总和
collect <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner); 将元素收集到类型 R 的结果中。在每个部分上,都会调用 supplier 来提供初始结果,调用 accumulator 来交替地将元素添加到结果中,并调用 combiner 来整合两个结果

基本类型流

可以将整数收集到 Stream<Integer> 中,但是将每个整数都包装到包装器对象中是很低效的。对其他基本类型来说,情况也是一样,这些基本类型是: doublefloatlongshortcharbyteboolean 。流库中具有专门的类型 IntStreamLongStreamDoubleStream ,用来直接存储基本类型值,而无需使用包装器。如果想要存储 shortcharbyteboolean ,可以使用 IntStream ,而对于 float ,可以使用 DoubleStream

为了创建 IntStream ,需要调用 IntStream.ofArrays.stream 方法:

IntStream stream = IntStream.of(1, 1, 2, 3, 5);
IntStream stream = Arrays.stream(arr);
IntStream stream = Arrays.stream(arr, from, to);

与对象流一样,我们还可以使用静态的 generateiterate 方法。此外 IntStreamLongStream 有静态方法 rangerangeClosed ,可以生成步长为 1 的整数范围:

IntStream zeroToNinetyNine = IntStream.range(0, 100);   // [0, 100)
IntStream zeroToHundred = IntStream.rangeClosed(0, 100);    // [0, 100]

CharSequence 接口拥有 codePointschars 方法,可以生成由字符的 Unicode 码或由 UTF-16 编码机制的码元构成的 IntStream

IntStream codes = sentence.codePoints();
IntStream chars = sentence.chars();

当你有 个对象流时,可以用 mapToIntmapToLongmapToDouble 将其转换为基本类型流。例如,如果你有一个字符串流,并想将其长度处理为整数,那么就可以在 IntStream 中实现:

Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);

为了将基本类型流转换为对象流,需要使 boxed 方法:

Stream<Integer> boxed = IntStream.range(0, 100).boxed();

通常,基本类型流上的方法与对象流上的方法类似。下面是最主要的差异:

  • toArray 方法会返回基本类型数组
  • 产生可选结果的方法会返回一个 OptionalIntOptionalLongOptionalDouble 。这些类与 Optional 类类似,但是具有 getAsIntgetAsLonggetAsDouble 方法,而不是 get 方法
  • 具有返回总和、平均值、最大值和最小值的 sumaveragemaxmin 方法。对象流没有定义这些方法
  • summaryStatistics 方法会产生一个类型为 IntSummaryStatisticsLongSummaryStatisticsDoubleSummaryStatistics 的对象,它们可以同时报告流的总和、平均值、最大值和最小值

Random 类具有 intslongsdoubles 方法,它们会返回由随机数构成的基本类型流

Random random = new Random();
IntStream intStream = random.ints();
LongStream longStream = random.longs();
DoubleStream doubleStream = random.doubles();
java.util.stream.IntStream 方法名称 方法声明 描述
range
rangeClosed
public static IntStream range(int startInclusive, int endExclusive)
public static IntStream rangeClosed(int startInclusive, int endInclusive)
产生一个由给定范围内的整数构成的 IntStream
of public static IntStream of(int... values) 产生一个由给定元素构成的 IntStream
toArray int[] toArray(); 产生一个由当前流中的元素构成的数组
sum
average
max
min
summaryStatistics
int sum();
OptionalDouble average();
OptionalInt max();
OptionalInt min();
IntSummaryStatistics summaryStatistics();
产生当前流中元素的总和、平均值、最大值和最小值,或者从中可以获得这些结果的所有四种值的对象
boxed Stream<Integer> boxed(); 产生用于当前流中的元素的包装器对象流

LongStreamDoubleStreamIntStream 具有相同的方法

java.lang.CharSequence 方法名称 方法声明 描述
codePoints public default IntStream codePoints() 产生由当前字符串的所有 Unicode 码点构成的流
java.util.Random 方法名称 方法声明 描述
ints
longs
doubles
public IntStream ints()
public IntStream ints(int randomNumberOrigin, int randomNumberBound)
public LongStream longs(long streamSize)
public IntStream ints(long streamSize, int randomNumberOrigin, int randomNumberBound)
产生随机数流 果提供了 streamSize ,这个流就是具有给定数量元素。当有限流提供了边界时,其元素将位于 randomNumberOrigin (包含)和 randomNumberBound(不包含)的区间内

longsdoublesints 具有相同的方法

java.util.OptionalInt 方法名称 方法声明 描述
of public static OptionalInt of(int value) 用所提供的基本类型值产生一个可选对象
getAsInt public int getAsInt() 产生当前可选对象的值,或者在其为空时抛出一个 NoSuchElementException 异常
orElse
orElseGet
orElseThrow
public int orElse(int other)
public int orElseGet(IntSupplier other)
public<X extends Throwable> int orElseThrow(Supplier<X> exceptionSupplier) throws X
产生当前可选对象的值,或者在这个对象为空时产生可替代的值,或者抛出异常
ifPresent public void ifPresent(IntConsumer consumer) 如果当前可选对象不为空, 将其值传递给 consumer

OptionalLongOptionalDoubleOptionalInt 具有相同的方法

java.util.IntSummaryStatistics 方法名称 方法声明 描述
getCount
getSum
getAverage
getMax
getMin
public final long getCount()
public final long getSum()
public final double getAverage()
public final int getMax()
public final int getMin()
产生收集到的元素的个数、总和、平均值、最大值和最小值

LongSummaryStatisticsDoubleSummaryStatisticsIntSummaryStatistics 具有相同的方法

并行流

流使得并行处理块操作变得很容易。这个过程几乎是自动的,但是需要遵守一些规则

可以用 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 返回的结果不行
  • 流操作的工作量应该具有较大的规模。如果总工作负载并不是很大,那么搭建并行计算时所付出的代价就没有什么意义
  • 流操作不应该被阻

换句话说,不要将所有的流都转换为并行流。只有在对已经位于内存中的数据执行大量计算操作时,才应该使用并行流

java.util.stream.BaseStream<T, S extends BaseStream<T, S>> 方法名称 方法声明 描述
parallel S parallel(); 产生一个与当前流中元素相同的并行流
unordered S unordered(); 产生一个与当前流中元素相同的无序流
java.util.Collection<E> 方法名称 方法声明 描述
parallelStream default Stream<E> parallelStream() 用当前集合中的元素产生一个并行流
posted @ 2022-04-24 21:17  流星<。)#)))≦  阅读(77)  评论(0编辑  收藏  举报