OnJava学习_Stream流

onJava – 流(2023.1.1)

概论

  • 集合优化了对象的存储,而流(stream)与对象的成批处理有关

  • 流是一个与任何特定的存储机制都没有关系的元素序列。事实上,我们说流 “没有存储”

  • 不同于在集合中遍历元素,使用流的时候,是从一个管道中抽取元素,并对它们进行操作。这些管道通常会被串联到一起,形成这个流上的一个操作管线

  • 大多数时候,我们将对象存储在一个集合中是为了处理它们,所以你会发现,自己编程的重点将从集合转向流

  • 流的一个核心优点是,它能使我们的程序更小,也更好理解。

  • 流可以对有状态的系统进行建模,而不需要使用赋值或可变数据

  • 示例

    // Randoms.java 没有声明任何变量。流可以对有状态的系统进行建模,而不需要使用赋值或可变数据
    public class Randoms {
        public static void main(String[] args) {
            // 1. Random(47) 对象设置了一个种子(这样程序每次运行都会得到相同的结果)
            // 2. ints(5, 20) 方法会生成一个流。该方法有多个重载版本,其中两个参数的版本可以设置所生成值的上下界
            // 3. distinct() 中间流操作,去除重复的值
            // 4. limit(7) 选择前7个值
            // 5. sorted() 让元素有序
            // 6. forEach() 方法会根据我们传递的函数,在每个流对象上执行一个操作
            new Random(47)
                    .ints(5, 20)
                    .distinct()
                    .limit(7)
                    .sorted()
                    .forEach(System.out::println);
        }
    }
    
  • 声明式编程是一种编程风格,我们说明想要完成什么(what),而不是指明怎么做(how)

  • 流编程的一个核心特性:内部迭代。内部迭代产生的代码不仅可读性更好,而且更容易利用多处理器:通过放宽对具体迭代方式的控制,我们可以将其交给某种并行化机制。

  • 流的另一个重要方面是惰性求值,这意味着它们只在绝对必要时才会被求值。我们可以把流想象成一个 “延迟列表” 。因为延迟求值,所以流使我们可以表示非常大的(甚至无限大的)序列,而不需要考虑内存问题

Java 8 对流的支持

  • Java 的设计者们面临一个难题。他们有一套现有的库,不仅用在了Java 库本身之中,还用在了用户编写的无数代码之中。他们是如何将流这个新的基本概念整合到现有的库中的呢?

    在类似 Random 这样简单的例子中,只需要添加更多方法即可。只要不修改现有方法,遗留代码就不受干扰。

    最大的挑战来自使用了接口的库。因为我们想将集合转换成流,所以集合类是至关重要的一部分。但如果向接口中加入新方法,就会破坏每一个实现了该接口,但没有实现这个新方法的类。

    Java 8 引入的解决方案是接口中的默认(default)方法。有了默认方法,Java 的设计者们可以将流方法硬塞进现有的类中,而且他们几乎把我们可能需要的每个操作都添加进去了。这些操作可分为三种类型:创建流、修改流元素(中间操作)和消费流元素(终结操作)。最后一种类型的操作往往意味着收集一个流的元素(通常是将其放进某个集合)

创建流

  1. Stream.of() : 轻松地将一组条目变成一个流

    // 1. Stream.of() 创建流
    public class StreamOf {
        public static void main(String[] args) {
            Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!").forEach(System.out::print);
    
            System.out.println();
            Stream.of(3.141592, 2.718, 1.618).forEach(System.out::println);
        }
    }
    
  2. 每个Collection都可以使用 stream() 方法来生成一个流

    // Collection有 stream() 方法来创建流
    public class CollectionStream {
        public static void main(String[] args) {
            List<Bubble> bubbles = Arrays.asList(
                    new Bubble(1), new Bubble(2), new Bubble(3));
            // map() 接受流中的每个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去
            // 普通的 map() 接受对象并生成对象,但当希望输出流持有的是数值类型的值时,map() 还有一些特殊版本
            // mapToInt() 将一个对象流转变成一个包含 Integer 的 IntStream。
            // 对于Float 和 Double,也有名字类似的操作
            System.out.println(
                    bubbles.stream().mapToInt(b -> b.i).sum()
            );
            
            Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
            w.stream().map(x -> x + " ").forEach(System.out::print);
            System.out.println();
    
            Map<String, Double> m = new HashMap<>();
            m.put("pi", 3.14159);
            m.put("e", 2.718);
            m.put("phi", 1.618);
            // 为了从Map 集合生成一个流,我们首先调用 entrySet() 来生成一个对象流,其中每个对象都包含着一个键和与其相关的值,
            // 然后再使用 getKey() 和 getValue() 将其分开
            m.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()).forEach(System.out::println);
        }
    }
    
  3. Random 类已经得到增强,有一组可以生成流的方法:

    • Random 类只会生成 int、long 和 double 等基本类型的值。boxed() 流操作会自动将基本类型转换为其对应的包装器类型
    // Random 类的生成流的方法
    public class RandomStream {
        // 为了消除冗余代码,创建了泛型方法show()
        public static <T> void show(Stream<T> stream) {
            stream.limit(4).forEach(System.out::println);
            System.out.println("--------------------------");
        }
    
        public static void main(String[] args) {
            Random rand = new Random(47);
            show(rand.ints().boxed());
            show(rand.longs().boxed());
            show(rand.doubles().boxed());
    
            // 控制上下边界:
            // 如:ints(10, 20) 表示 大于等于10 小于20 的随机数
            show(rand.ints(10, 20).boxed());
            show(rand.longs(50, 100).boxed());
            show(rand.doubles(20, 30).boxed());
    
            // 控制流的大小:
            // ints(2) 表示 生成 2 两个随机数
            show(rand.ints(2).boxed());
            show(rand.longs(2).boxed());
            show(rand.doubles(2).boxed());
    
            // 控制流的大小和边界:
            // ints(3, 3, 9) 表示 生成 3(第一个参数) 个 大于等于3 小于9 的随机数
            show(rand.ints(3, 3, 9).boxed());
            show(rand.longs(3, 12, 22).boxed());
            show(rand.doubles(3, 11.5, 12.3).boxed());
        }
    }
    
  4. IntStream 类提供了一个 range() 方法,可以生成一个流—— 由int 值组成的序列。这在编写循环时非常方便:

    // IntStream 的 range() 方法
    public class Ranges {
        public static void main(String[] args) {
            // 传统方式:
            int result = 0;
            for (int i = 10; i < 20; i++)
                result += i;
            System.out.println(result);
    
            // for-in 搭配一个区间范围:
            result = 0;
            for(int i : range(10, 20).toArray())
                result += i;
            System.out.println(result);
    
            // 使用流:
            System.out.println(range(10, 20).sum());
        }
    }
    
  5. Stream.generate() : generate() 方法中的参数为 Supplier

    // Stream.generate()
    public class Generate {
        public static void main(String[] args) {
            // bubbler() 与 Supplier<Bubble> 接口兼容,所以可以将其方法引用传给Stream.generate()
            Stream.generate(Bubble::bubbler)
                    .limit(5)
                    .forEach(System.out::println);
    
            Stream.generate(() -> "duplicate")
                    .limit(3)
                    .forEach(System.out::println);
        }
    }
    
  6. Stream.iterate() : 从一个种子开始(第一个参数),然后将其传给第二个参数所引用的方法。其结果被添加到这个流上,并且保存下来作为下一次 iterate() 调用的第一个参数,以此类推

    // Stream.iterate() 创建流,并应用于斐波那契数列
    public class IterateFibonacci {
        int x = 1;
        
        // 斐波那契数列将数列中的最后两个元素相加,生成下一个元素。
        // iterate() 只会记住结果(result),所以必须使用 x 来记住另一个元素
        Stream<Integer> numbers() {
            return Stream.iterate(0, i -> {
                int result = x + i;
                x = i;
                return result;
            });
        }
    
        public static void main(String[] args) {
            new IterateFibonacci().numbers()
                    .skip(20)   // 不使用前20个
                    .limit(10)  // 然后从中取10个
                    .forEach(System.out::println);
        }
    }
    
  7. 流生成器:在生成器(Builder)设计模式中,我们创建一个生成器对象,为它提供多段构造信息,最后执行 “生成” (build)动作。Stream库提供了这样一个 Builder

    // 流生成器
    public class FileToWordsBuilder {
        Stream.Builder<String> builder = Stream.builder();
    
        public FileToWordsBuilder(String filePath) throws IOException {
            Files.lines(Paths.get(filePath))
                    .skip(1)
                    .forEach(line -> {
                        for (String w : line.split("[ .?,]+"))
                            builder.add(w);
                    });
        }
    
        Stream<String> stream() { return builder.build();}
    
        // 注意,构造器添加了文件中的所有单词(除了第一行,这是一个包含了文件路径信息的注释)
        // 但是它没有调用 build()。这意味着,只要不调用 stream(),就可以继续向builder 对象中添加单词。
        // 如果希望这个类更完整,可以加入一个标志来查看 build() 是否已经被调用,加入一个方法在可能的情况下继续添加单词
        // 如果在调用 build() 之后还尝试向 Stream.Builder 中添加单词,则会产生异常
        public static void main(String[] args) throws IOException {
            new FileToWordsBuilder("E:\\onJava\\ch14\\src\\resources\\stream\\Cheese.dat").stream()
                    .limit(7)
                    .map(w -> w + " ")
                    .forEach(System.out::print);
        }
    }
    
  8. Arrays 类中包含了名为 stream() 的静态方法,可以将数组转换为流。stream() 方法可以生成 IntStream、LongStream 和 DoubleStream :

    // Arrays 的 stream() 静态方法
    public class ArraysStream {
        public static void main(String[] args) {
            Arrays.stream(
                    new double[] {3.14159, 2.718, 1.618})
                    .forEach(n -> System.out.format("%f ", n));
            System.out.println();
    
            Arrays.stream(
                    new int[] {1, 3, 5})
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
    
            Arrays.stream(
                    new long[] {11, 22, 44, 66})
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
    
            // 选择一个子区间:
            // 使用了两个额外的参数:第一个告诉 stream() 从数组的哪个位置开始选择元素,第二个告诉它在哪里停止
            Arrays.stream(
                    new int[] {1, 3, 5, 7, 15, 28, 37}, 3, 6)
                    .forEach(n -> System.out.format("%d ", n));
        }
    }
    
  9. Pattern 类的 splitAsStream() :Java 8 向java.util.regex.Pattern 类中加入了一个新方法splitAsStream() ,它接受一个字符序列,并根据我们传入的公式将其分割为一个流。这里有一个约束:splitAsStream() 的输入应该是一个 CharSequence,所以我们不能将一个流传到splitAsStream() 中。

    // Pattern 的 splitAsStream()
    public class PatternSplitAsStream {
        private String all;
        public PatternSplitAsStream(String filePath) throws IOException {
            all = Files.lines(Paths.get(filePath))
                    .skip(1)
                    .collect(Collectors.joining(" "));
        }
    
        public Stream<String> stream() {
            return Pattern.compile("[ .,?]+")
                    .splitAsStream(all);
        }
    
        public static void main(String[] args) throws IOException {
            PatternSplitAsStream ps = new PatternSplitAsStream("E:\\onJava\\ch14\\src\\resources\\stream\\Cheese.dat");
    
            ps.stream().limit(7).map(w -> w + " ").forEach(System.out::print);
            ps.stream().skip(7).limit(2).map(w -> w + " ").forEach(System.out::print);
    
        }
    }
    

中间操作

  • 这些操作从一个流接收对象,并将对象作为另一个流送出后端,以连接到其他操作
  1. 跟踪与调试:peek()

    • peek() 操作就是用来辅助调试的。它允许我们查看流对象而不修改它们:

      // 因为 peek() 接受的是一个遵循Consumer 函数式接口的函数,这样的函数没有返回值,
      // 所以也就不可能用不同的对象替换流中的对象。我们只能 “看看” 这些对象
      public class Peeking {
          public static void main(String[] args) throws IOException {
              FileToWords.stream("E:\\onJava\\ch14\\src\\resources\\stream\\Cheese.dat")
                      .skip(21)
                      .limit(4)
                      .map(w -> w + " ")
                      .peek(System.out::print)
                      .map(String::toUpperCase)
                      .peek(System.out::print)
                      .map(String::toLowerCase)
                      .forEach(System.out::print);
          }
      }
      
  2. 对流元素进行排序:sorted()

    • 在Randoms.java 中看到过以默认的比较方式使用的 sorted() 进行排序的情况。还有一种接受Comparator 参数的sorted() 形式:

      // 可以传入一个Lambda 函数作为sorted() 的参数,不过也有预先定义好的Comparator,
      // 这里就使用了一个逆转 “自然排序” 的    
      public class SortedComparator {
          public static void main(String[] args) throws IOException {
              FileToWords.stream(FilePath.FILE_PATH)
                      .skip(10)
                      .limit(10)
                      .sorted(Comparator.reverseOrder())
                      .map(w -> w + " ")
                      .forEach(System.out::print);
          }
      }
      
  3. 移除元素:

    1. distinct() : 移除流中的重复元素。与创建一个Set 来消除重复元素相比,使用distinct() 要省力得多。Randoms.java 中有示例
    2. filter ( Predicate ) : 过滤操作只保留符合特定条件的元素,也就是传给参数(即过滤函数),结果为 true 的那些元素
    public class Prime {
        // 过滤函数 isPrime() 会检测素数
        // rangeClosed() 包含了上界值。
        // 如果没有任何一个取余操作的结果为 0,则noneMatch() 操作返回true
        // 如果有一个计算结果等于 0,则返回false。
        // noneMatch() 会在第一次失败之后退出,而不会把后面的所有计算都尝试一遍
        public static boolean isPrime(long n) {
            return rangeClosed(2, (long) Math.sqrt(n)).noneMatch(i -> n % i == 0);
        }
    
        public LongStream numbers() {
            return iterate(2, i -> i + 1).filter(Prime::isPrime);
        }
    
        public static void main(String[] args) {
            new Prime().numbers()
                    .limit(10)
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
    
            new Prime().numbers()
                    .skip(90)
                    .limit(10)
                    .forEach(n -> System.out.format("%d ", n));
        }
    }
    
  4. 将函数应用于每个流元素:

    1. map (Function) : 将Function 应用于输入流中的每个对象,结果作为输出流继续传递
    2. mapToInt (ToIntFunction) : 同上,不过结果放在一个IntStream 中。
    3. mapToLong (ToLongFunction) : 同上,不过结果放在一个LongStream 中。
    4. mapToDouble (ToDoubleFunction) : 同上,不过结果放在一个DoubleStream 中。
    // 注意,map() 将一个String 映射到了一个 其他类型,
    // 这里没有要求生成的类型必须与输入的类型相同,所以可以在这里改变这个流的类型
    // 如果 Function 生成的结果类型是某种数值类型,必须使用相对应的mapTo 操作来代替    
    public class FunctionMap {
        public static void main(String[] args) {
            Stream.of("5", "7", "9")
                    .mapToInt(Integer::parseInt)
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
    
            Stream.of("17", "19", "23")
                    .mapToLong(Long::parseLong)
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
    
            Stream.of("17", "1.9", ".23")
                    .mapToDouble(Double::parseDouble)
                    .forEach(n -> System.out.format("%f ", n));
        }
    }
    
  5. 在应用map() 期间组合流

    • 假设有一个由传入元素组成的流,我们正在其上应用一个 map() 函数,这个函数有一些功能上的独特优势,但是存在一个问题:它生成的是一个流。换句话说,我们想要的是一个由元素组成的流,但生成了一个由元素流组成的流
    • flatMap() 会做两件事:接受生成流的函数,并将其应用于传入元素(就像 map() 所做的那样),然后再将每个流 “扁平化” 处理,将其展开为元素。所以传出来的就都是元素了。
    1. flatMap (Function) : 当Function 生成的是一个流时使用
    2. flatMapToInt (Function) : 当Function 生成的是一个 IntStream 流时使用
    3. flatMapToLong (Function) : 当Function 生成的是一个 LongStream 流时使用
    4. flatMapToDouble (Function) : 当Function 生成的是一个 DoubleStream 流时使用
    // flatMap() 和 map() 的比较
    public class FlatMap {
        public static void main(String[] args) {
            // 得到的是一个由指向其他流的 “头” 组成的流
            Stream.of(1, 2, 3)
                    .map(i -> Stream.of("Gonzo", "Kermit", "Beaker"))
                    .map(e -> e.getClass().getName())
                    .forEach(System.out::println);     // 输出:java.util.stream.ReferencePipeline$Head
    
            // 从flatMap() 返回的每个流都会被自动扁平化处理,展开为组成这个流的 String 元素
            Stream.of(1, 2, 3)
                    .flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
                    .forEach(System.out::println);
        }
    }
    

Optional 类型

  • 在研究终结操作之前,我们必须考虑一个问题:如果我们向流中请求对象,但是流中的什么都没有,这时会发生什么呢?我们喜欢把流连接成 “快乐通道” (指没有异常或错误情形发生的默认场景),并假设没有什么会中断它。然而在流中放入一个 null 就能轻松破坏它。

    有没有某种我们可以使用的对象,既可以作为流元素占位,也可以在我们要找的元素不存在时友好地告知我们(也就是说,不会抛出异常)?

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

    • findFirst () : 返回包含第一个元素的 Optional。如果这个流为空,则返回 Optional.empty
    • findAny () : 返回包含任何元素的 Optional,如果这个流为空,则返回 Optional.empty
    • max()、min() : 分别返回包含流中最大值和最小值的 Optional,如果这个流为空,则返回 Optional.empty
    • reduce() : 有一个版本的 reduce() ,它并不以一个 “identity” 对象作为其第一个参数(在 reduce() 的其他版本中,“identity” 对象会成为默认结果,所以不会有结果为空的风险),它会将返回值包在一个 Optional 中
    • average() : 对于数值化的流 IntStream、LongStream 和 DoubleStream,average() 操作将其结果包在一个 Optional 中,以防流为空的情况
    // Stream.<String>empty() 可以创建 空流。如果只用Stream.empty()而没有任何上下文信息,Java 无法知道它应该是什么类型
    public class OptionalsFromEmptyStreams {
        public static void main(String[] args) {
            // 这时不会因为流是空的而抛出异常,而是会得到一个 Optional.empty 对象
            // Optional 有一个 toString() 方法,可以显示有用的信息
            System.out.println(Stream.<String>empty().findFirst());
    
            System.out.println(Stream.<String>empty().findAny());
    
            System.out.println(Stream.<String>empty().max(String.CASE_INSENSITIVE_ORDER));
    
            System.out.println(Stream.<String>empty().min(String.CASE_INSENSITIVE_ORDER));
    
            System.out.println(Stream.<String>empty().reduce((s1, s2) -> s1 + s2));
    
            System.out.println(IntStream.empty().average());
        }
    }
    
  • Optional 的两个基本动作:

    1. isPresent() : 查看 Optional 中是否有东西
    2. get() : 获取 Optional 中的东西
    public class OptionalBasics {
        static void test(Optional<String> optString) {
            if (optString.isPresent())
                System.out.println(optString.get());
            else
                System.out.println("Nothing inside");
        }
    
        public static void main(String[] args) {
            test(Stream.of("Epithets").findFirst());
            test(Stream.<String>empty().findFirst());
        }
    }
    

便捷函数

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

    1. ifPresent(Consumer) : 如果值存在,则用这个值来调用 Consumer,否则什么都不做
    2. orElse(otherObject) : 如果对象存在,则返回这个对象,否则返回 otherObject
    3. orElseGet(Supplier) : 如果对象存在,则返回这个对象,否则返回使用 Supplier 函数创建的替代对象
    4. orElseThrow(Supplier) : 如果对象存在,则返回这个对象,否则抛出一个使用 Supplier 函数创建的异常

创建 Optional

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

    1. empty() : 返回一个空的 Optional
    2. of(value) : 如果已经知道这个value 不是 null,可以使用该方法将其包在一个 Optional 中
    3. ofNullable(value) : 如果不知道这个value 是不是 null,使用这个方法。如果value 为 null,它会自动返回 Optional.empty,否则会将这个value 包在一个 Optional 中
    public class CreatingOptionals {
        static void test(String testName, Optional<String> opt) {
            System.out.println("====" + testName + "====");
            System.out.println(opt.orElse("Null"));
        }
    
        public static void main(String[] args) {
            test("empty", Optional.empty());
            test("of", Optional.of("Howdy"));
            
            // 如果试图通过向 of() 传递null 来创建Optional,它会抛出空指针异常。
            // ofNullable() 可以优雅地处理null,所以它看起来是最安全的一个
            try{
                test("of", Optional.of(null));
            } catch (Exception e) {
                System.out.println(e);
            }
            test("ofNullable", Optional.ofNullable("Hi"));
            test("ofNullable", Optional.ofNullable(null));
        }
    }
    

Optional 对象上的操作

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

    1. filter( Predicate) : 将 Predicate 应用于 Optional 的内容,并返回其结果。如果 Optional 与 Predicate 不匹配,则将其转换为 empty。如果 Optional 本身已经是 empty,则直接传回
    2. map( Function ) : 如果 Optional 不为 empty,则将 Function 应用于 Optional 中包含的对象,并返回结果。否则传回 Optional.empty
    3. flatMap( Function ) : 和map() 类似,但是所提供的映射函数会将结果包在 Optional 中,这样 flatMap() 最后就不会再做任何包装了
    4. 数值化的 Optional 上没有提供这些操作
  • filter ( Predicate): 对于普通的流 filter() 而言,如果 Predicate 返回 false,它会将元素从流中删除。但是对于 Optional.filter() 而言,如果 Predicate 返回false,它不会删除元素,但是会将其转化为 empty。

    public class OptionalFilter {
        static String[] elements = {"Foo", "", "Bar", "Baz", "Bingo"};
        
        static Stream<String> testStream() {
            return Arrays.stream(elements);
        }
        
        static void test(String descr, Predicate<String> pred) {
            System.out.println("----( " + descr + " )----");
            // 每次进入 for 循环,它都会重新获得一个流,并跳过用for循环的索引设置的元素数,这就使其看上去像流中的连续元素
            // 然后执行 findFirst(),获取剩余元素中的第一个,它会被包在一个 Optional 中返回
            for (int i = 0; i < elements.length; i++) {
                System.out.println(
                        testStream().skip(i)
                                    .findFirst()
                                    .filter(pred)
                );
            }
        }
    
        public static void main(String[] args) {
            test("true", str -> true);
            test("false", str -> false);
            test("str != \"\"", str -> str != "");
            test("str.length == 3", str -> str.length() == 3);
        }
    }
    
  • 类似于map (),Optional.map() 会应用一个函数,但是在 Optional 的情况下,只有当 Optional 不为 empty 时,它才会应用这个映射函数。它也会提取 Optional 所包含的对象,并将其交给映射函数。映射函数的结果会自动地包在一个 Optional 中。

  • Optional 的 flatMap() 被应用于已经会生成 Optional 的映射函数,所以 flatMap() 不会像 map() 那样把结果包在 Optional 中。map() 和 flatMap() 的唯一区别是:flatMap() 不会将结果包在 Optional 中给,因为这个事映射函数已经做了

由 Optional 组成的流

  • 假设由一个可能会生成null 值的生成器。如果使用这个生成器创建了一个流,我们自然想将这些元素包在 Optional 中。

    public class Signal {
        private final String msg;
        
        public Signal(String msg) {
            this.msg = msg;
        }
    
        public String getMsg() {
            return msg;
        }
    
        @Override
        public String toString() {
            return "Signal(" + msg + ")";
        }
        
        static Random random = new Random(47);
        public static Signal morse() {
            switch (random.nextInt()) {
                case 1: return new Signal("dot");
                case 2: return new Signal("dash");
                default: return null;
            }
        }
        
        // 生成一个 Optional 流
        public static Stream<Optional<Signal>> stream() {
            return Stream.generate(Signal::morse)
                    .map(signal -> Optional.ofNullable(signal));
        }
    
        public static void main(String[] args) {
            Signal.stream()
                    .limit(10)
                    .forEach(System.out::println);
            System.out.println("--------------------------------");
            
            // 这里使用 filter(),只保留非 empty 的Optional,然后通过map() 调用get() 来获取包在其中的对象
            Signal.stream()
                    .limit(10)
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .forEach(System.out::println);
        }
    }
    

终结操作

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

将流转换为一个数组

  • toArray() : 将流元素转换到适当类型的数组中

  • toArray(generator) : generator 用于在特定情况下分配自己的数组存储

    // 假设我们想获得随机数,同时希望以流的形式复用它们,这样我们每次得到的都是相同的流。
    // 我们可以将其保存在一个数组中,来实现这个目的
    public class RandInts {
        private static int[] rints = 
                new Random(47).ints(0, 1000).limit(100).toArray();
        
        // 该方法在下文会多次用到
        public static IntStream rands() {
            return Arrays.stream(rints);
        }
    }
    

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

  1. forEach (Consumer) : 这个用法已经看过很多次了——以 System.out::println 作为Consumer 函数
  2. forEachOrdered (Consumer) : 这个版本确保 forEach 对元素的操作顺序是原始的流的顺序
  • 第一种形式被明确地设计为可以以任何顺序操作元素,这只有在引入 parallel() 操作时才有意义。parallel() 让Java 尝试在多个处理器上执行操作。

    import static finallyOperation.RandInts.*;
    
    public class ForEach {
        static final int SZ = 14;
    
        public static void main(String[] args) {
            // 输出:258 555 693 861 961 429 868 200 522 207 288 128 551 589 
            rands().limit(SZ)
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
    
            // 输出:551 589 861 200 522 288 429 128 555 693 868 258 207 961 
            rands().limit(SZ)
                    .parallel()
                    .forEach(n -> System.out.format("%d ", n));
            System.out.println();
            
            // 仍然使用了 parallel(),但是又使用forEachOrdered() 来**强制结果回到**原始的顺序
            // 因此,对于非 parallel() 的流,使用 forEachOrdered() 不会有任何影响
            // 输出:258 555 693 861 961 429 868 200 522 207 288 128 551 589 
            rands().limit(SZ)
                    .parallel()
                    .forEachOrdered(n -> System.out.format("%d ", n));
        }
    }
    

收集操作

  1. collect (Collector) : 使用这个Collector 将流元素累加到一个结果集合中
  2. collect (Supplier,BiConsumer,BiConsumer) :和上面类似,但是Supplier 会创建一个新的结果集合,第一个BiConsumer 是用来将下一个元素包含在结果中的函数,第二个BiConsumer 用于将两个值组合起来
  • 在 java.util.stream.Collectors 类中有很多生成 Collector 的方法:to*(),如toMap(),可以满足一般情况下的需要。但是有部分Collector没有,不过可以使用 Collectors.toConllection() 并将任何类型的 Collection 的构造器引用传给它

    // 在Collectors 中没有特定的toTreeSet() 方法,可以使用 Collectors.toCllection(TreeSet::new)
    public class Collect {
        public static void main(String[] args) throws IOException {
            Set<String> words = Files.lines(Paths.get("E:\\onJava\\ch14\\src\\finallyOperation\\Collect.java"))
                                        .flatMap(s -> Arrays.stream(s.split("\\W+")))
                                        .filter(s -> !s.matches("\\d+"))
                                        .map(String::trim)
                                        .filter(s -> s.length() > 2)
                                        .limit(100)
                                        .collect(Collectors.toCollection(TreeSet::new));
            System.out.println(words);
        }
    }
    
  • 大多数情况下,如果看一下 java.util.stream.Collectors,就能找到一个满足我们要求的预定的Collector。找不到的情况只是极少数,这时候可以使用 collect() 的第二种形式。

    // 我们通常不需要第二种形式
    public class Collect2 {
        public static void main(String[] args) throws IOException {
            ArrayList<String> words = FileToWords.stream(FilePath.FILE_PATH)
                                                    .collect(ArrayList::new,
                                                             ArrayList::add,
                                                             ArrayList::addAll);
    
            words.stream().filter(s -> s.equals("cheese")).forEach(System.out::println);
        }
    }
    

组合所有的流元素

  1. reduce (BinaryOperator) : 使用 BinaryOperator 来组合所有的流元素。因为这个流可能为空,所以返回的是一个 Optional
  2. reduce (identity,BinaryOperator) : 和上面一样,但是将identity 用作这个组合的初始值,因此,即使这个流是空的,我们仍然能得到identity 作为结果。
  3. reduce (identity,BiFunction,BinaryOperator) : 这个更复杂,它可能更高效,不过平时用得少。可以通过组合显示的map() 和 reduce() 操作来更简单地表达这样的需求。
class Frobnitz {
    int size;
    Frobnitz(int sz) { size = sz; }
    @Override
    public String toString() {
        return "Frobnitz(" + size + ")";
    }

    // 生成器:
    static Random rand = new Random(47);
    static final int BOUND = 100;
    static Frobnitz supply() {
        return new Frobnitz(rand.nextInt(BOUND));
    }
}

// 在使用reduce() 时,没有提供作为 ”初始值“ 的第一个参数,这意味着它会生成一个 Optional
// 下面的reduce() 中的 Lambda 表达式中的第一个参数 fr0 是上次调用这个 reduce() 时带回来的结果,第二个参数 fr1 是来自流中的新值。
// reduce() 中的 Lambda 表达式使用了一个三元选择操作符,如果 fr0 的 size 小于 50,就接受 fr0,否则就接受 fr1,也就是序列中的下一个元素。
// 作为结果,我们得到的是流中 **第一个** size小于50的Frobnitz ———— 一旦找到了这样的对象,它就会抓住不放,哪怕还会出现其他候选
// 尽管这个约束相当奇怪,但它确实让我们对reduce() 有了更多的了解
public class Reduce {
    public static void main(String[] args) {
        Stream.generate(Frobnitz::supply)
                .limit(10)
                .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
                .ifPresent(System.out::println);
    }
}

匹配

  1. allMatch (Predicate) : 当使用所提供的 Predicate 检测流中的元素时,如果每一个元素都得到 true,则返回 true。在遇到第一个 false 时,会短路计算。也就是说,在找到一个 false 之后,它不会继续计算
  2. anyMatch (Predicate) : 当使用所提供的 Predicate 检测流中的元素时,如果有任何一个元素能得到 true,则返回 true。在遇到第一个 true 时,会短路计算
  3. noneMatch (Predicate) : 当使用所提供的 Predicate 检测流中的元素时,如果没有元素得到 true,则返回 true。在遇到第一个 true 时,会短路计算
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);
    }
}

选择一个元素

  1. findFirst () : 返回一个包含流中第一个元素的 Optional,如果流中没有元素,则返回 Optional.empty
  2. findAny () : 返回一个包含流中某个元素的 Optional,如果流中没有元素,则返回 Optional.empty
  • findFirst () 总是会选择流中的第一个元素,不管该流是否为并行的(即通过 Parallel () 获得的流)
  • 对于非并行的流,findAny () 会选择第一个元素(尽管从定义来看,它可以选择任何一个元素)。当这个流是并行流时,findAny () 有可能选择第一个元素以外的其他元素
import static finallyOperation.RandInts.*;

public class SelectElement {
    public static void main(String[] args) {
        System.out.println(rands().findFirst().getAsInt());     // 输出:258
        System.out.println(
                rands().parallel().findFirst().getAsInt()       // 输出:258
        );

        System.out.println(rands().findAny().getAsInt());       // 输出:258
        System.out.println(
                rands().parallel().findAny().getAsInt()         // 输出:242
        );
    }
}
  • 如果必须选择某个流的最后一个元素,请使用 reduce ()

    // reduce() 的参数(即 (n1, n2) -> n2 )是用两个元素中的后一个元素替换了这两个,这样最终得到的就是流中的最后一个元素了
    // 如果流是数值型的,则必须使用适当的数值化 Optional 类型,如下面的 IntOptional
    // 否则就要像 Optional<String> 中这样使用一个类型化的 Optional    
    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));
    
            // 非数值对象:
            Optional<String> lastObj = Stream.of("one", "two", "three")
                                                .reduce((n1, n2) -> n2);
            System.out.println(lastObj.orElse("Nothing"));
        }
    }
    

获取流相关的信息

  1. count () : 获取流中元素的数量
  2. max (Comparator) : 通过 Comparator 确定这个流中的 “最大” 元素,返回值为 Optional
  3. min (Comparator) : 通过 Comparator 确定这个流中的 “最小” 元素,返回值为 Optional
public class Informational {
    public static void main(String[] args) throws IOException {
        System.out.println(
                FileToWords.stream(FilePath.FILE_PATH).count());

        System.out.println(
                FileToWords.stream(FilePath.FILE_PATH).min(String.CASE_INSENSITIVE_ORDER)
                            .orElse("NONE"));

        System.out.println(
                FileToWords.stream(FilePath.FILE_PATH).max(String.CASE_INSENSITIVE_ORDER)
                        .orElse("NONE"));
    }
}
  • 获得数值化流相关的信息

    1. average () : 就是通常的意义,获得平均数
    2. max () 和 min () :这些操作不需要一个 Comparator ,因为它们处理的是数值化流
    3. sum () : 将流中的累加起来
    4. summaryStatistics () : 返回可能有用的摘要数据。不太清楚为什么Java 库的设计者觉得需要这个,因为我们自己可以用直接方法获得所有这些数据
    import static finallyOperation.RandInts.*;
    
    public class NumberStreamInfo {
        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());
        }
    }
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值