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 的设计者们可以将流方法硬塞进现有的类中,而且他们几乎把我们可能需要的每个操作都添加进去了。这些操作可分为三种类型:创建流、修改流元素(中间操作)和消费流元素(终结操作)。最后一种类型的操作往往意味着收集一个流的元素(通常是将其放进某个集合)
创建流
-
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); } }
-
每个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); } }
-
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()); } }
-
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()); } }
-
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); } }
-
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); } }
-
流生成器:在生成器(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); } }
-
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)); } }
-
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); } }
中间操作
- 这些操作从一个流接收对象,并将对象作为另一个流送出后端,以连接到其他操作
-
跟踪与调试: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); } }
-
-
对流元素进行排序: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); } }
-
-
移除元素:
- distinct() : 移除流中的重复元素。与创建一个Set 来消除重复元素相比,使用distinct() 要省力得多。Randoms.java 中有示例
- 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)); } }
-
将函数应用于每个流元素:
- map (Function) : 将Function 应用于输入流中的每个对象,结果作为输出流继续传递
- mapToInt (ToIntFunction) : 同上,不过结果放在一个IntStream 中。
- mapToLong (ToLongFunction) : 同上,不过结果放在一个LongStream 中。
- 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)); } }
-
在应用map() 期间组合流
- 假设有一个由传入元素组成的流,我们正在其上应用一个 map() 函数,这个函数有一些功能上的独特优势,但是存在一个问题:它生成的是一个流。换句话说,我们想要的是一个由元素组成的流,但生成了一个由元素流组成的流
- flatMap() 会做两件事:接受生成流的函数,并将其应用于传入元素(就像 map() 所做的那样),然后再将每个流 “扁平化” 处理,将其展开为元素。所以传出来的就都是元素了。
- flatMap (Function) : 当Function 生成的是一个流时使用
- flatMapToInt (Function) : 当Function 生成的是一个 IntStream 流时使用
- flatMapToLong (Function) : 当Function 生成的是一个 LongStream 流时使用
- 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 的两个基本动作:
- isPresent() : 查看 Optional 中是否有东西
- 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 中的数据,它们简化了上面 “先检查再处理所包含的对象” 的过程
- ifPresent(Consumer) : 如果值存在,则用这个值来调用 Consumer,否则什么都不做
- orElse(otherObject) : 如果对象存在,则返回这个对象,否则返回 otherObject
- orElseGet(Supplier) : 如果对象存在,则返回这个对象,否则返回使用 Supplier 函数创建的替代对象
- orElseThrow(Supplier) : 如果对象存在,则返回这个对象,否则抛出一个使用 Supplier 函数创建的异常
创建 Optional
-
当需要自己编写生成 Optional 的代码时,有如下三种可以使用的静态方法:
- empty() : 返回一个空的 Optional
- of(value) : 如果已经知道这个value 不是 null,可以使用该方法将其包在一个 Optional 中
- 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,你可以在最后再做一项处理
- filter( Predicate) : 将 Predicate 应用于 Optional 的内容,并返回其结果。如果 Optional 与 Predicate 不匹配,则将其转换为 empty。如果 Optional 本身已经是 empty,则直接传回
- map( Function ) : 如果 Optional 不为 empty,则将 Function 应用于 Optional 中包含的对象,并返回结果。否则传回 Optional.empty
- flatMap( Function ) : 和map() 类似,但是所提供的映射函数会将结果包在 Optional 中,这样 flatMap() 最后就不会再做任何包装了
- 数值化的 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); } }
在每个流元素上应用某个终结操作
- forEach (Consumer) : 这个用法已经看过很多次了——以 System.out::println 作为Consumer 函数
- 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)); } }
收集操作
- collect (Collector) : 使用这个Collector 将流元素累加到一个结果集合中
- 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); } }
组合所有的流元素
- reduce (BinaryOperator) : 使用 BinaryOperator 来组合所有的流元素。因为这个流可能为空,所以返回的是一个 Optional
- reduce (identity,BinaryOperator) : 和上面一样,但是将identity 用作这个组合的初始值,因此,即使这个流是空的,我们仍然能得到identity 作为结果。
- 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);
}
}
匹配
- allMatch (Predicate) : 当使用所提供的 Predicate 检测流中的元素时,如果每一个元素都得到 true,则返回 true。在遇到第一个 false 时,会短路计算。也就是说,在找到一个 false 之后,它不会继续计算
- anyMatch (Predicate) : 当使用所提供的 Predicate 检测流中的元素时,如果有任何一个元素能得到 true,则返回 true。在遇到第一个 true 时,会短路计算
- 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);
}
}
选择一个元素
- findFirst () : 返回一个包含流中第一个元素的 Optional,如果流中没有元素,则返回 Optional.empty
- 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")); } }
获取流相关的信息
- count () : 获取流中元素的数量
- max (Comparator) : 通过 Comparator 确定这个流中的 “最大” 元素,返回值为 Optional
- 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"));
}
}
-
获得数值化流相关的信息
- average () : 就是通常的意义,获得平均数
- max () 和 min () :这些操作不需要一个 Comparator ,因为它们处理的是数值化流
- sum () : 将流中的累加起来
- 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()); } }