Java流(Stream、Optional)详解

1. Java8对流的支持

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

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

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

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(),可以轻松地将一组条目变成一个流:

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() 方法来生成一个流:

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 引入了流,有一组可以生成流的方法:

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 值组成的序列),在编写循环时非常方便:

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 循环:

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

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() 调用的第一个参数,以此类推。我们可以通过迭代生成一个之前实现过的斐波那契数列:

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,假设我们实现读取文件的每一行并将其转换为单词流:

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
         */
    }
}

其中的文本内容如下:

Hello world!
Today is Saturday?
Have a nice day!

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

2.6 Arrays

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

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

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 的流元素。

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

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 中,以防流为空的情况。

看一下以下代码样例:

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() 调用的类型,就像下面这样:

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

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

4.1 便捷函数

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

  • ifPresent(Consumer):如果对象存在,则用这个对象来调用 Consumer,否则什么都不做。
  • orElse(otherObject):如果对象存在,则返回这个对象,否则返回 otherObject
  • orElseGet(Supplier):如果对象存在,则返回这个对象,否则返回使用 Supplier 函数创建的替代对象
  • orElseThrow(Supplier):如果对象存在,则返回这个对象,否则抛出一个使用 Supplier 函数创建的异常
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 中。

来看一下示例:

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 的用法:

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 中的对象:

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 用于在特定情况下分配自己的数组存储。

直接看样例:

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() 的作用和必要性:

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 中:

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 文件内容如下:

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() 操作来更简单地表达这种需求。

来看一下最简单的用法:

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,一旦找到了一个这样的对象,它就会抓住不放,哪怕还会出现其他候选。

猜你喜欢

转载自blog.csdn.net/m0_51755720/article/details/134087431