本文介绍了 Java8 的一个十分强大的功能:流(Stream),能够优化传统实现方式中复杂冗余的代码。
1. Java8对流的支持
流是一个与任何特定的存储机制都没有关系的元素序列。不同于在集合中遍历元素,使用流的时候,我们是从一个管道中抽取元素,并对它们逬行操作。这些管道通常会被串联到一起,形成这个流上的一个操作管线。
流的一个核心优点是,它们能使我们的程序更小,也更好理解。当配合流使用时,Lambda 表达式和方法引用就发挥出其威力了。流大大提升了 Java 8 的吸引力。
假设我们想按照有序方式显示随机选择的5~20范围内的,不重复的整数,借助流我们只需要说明想做什么即可实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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); } }
我们先为 Random
对象设置一个随机种子,ints()
方法会生成一个流,该方法有多个重载版本,其中两个参数的版本可以设置所生成值的上下界。使用中间流操作 distinct()
去掉重复的值,再使用 limit()
选择前5个值,sorted()
表示元素是有序的,最后我们想显示每一个条目,所以使用了 forEach()
,它会根据我们传递的函数,在每个流对象上执行一个操作。这里我们传递了一个方法引用 System.out::println
,用于将每个条目显示在控制台上。
使用流实现的代码我们看不到任何显式的迭代机制,因此称为内部迭代 ,这是流编程的一个核心特性。
2. 流的创建
使用 Stream.of()
,可以轻松地将一组条目变成一个流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); } }
此外,每个 Collection
都可以使用 stream()
方法来生成一个流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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); } }
在创建了一个 List<A>
之后,只需要调用一下 stream()
这个所有集合类都有的方法。中间的 map()
操作接受流中的毎个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去,这里的 mapToInt()
将一个对象流转变成了一个包含 Integer
的 IntStream
。对于 Float
和 Double
也有名字类似的操作。
collect()
操作会根据其参数将所有的流元素组合起来,当我们使用 Collectors.joining()
时,得到的结果是一个 String
,每个元素都会以 joining()
的参数分隔,还有其他很多 Collectors
,可以生成不同的结果。
为了从 Map
集合生成一个流,我们首先调用 entrySet()
来生成一个对象流,其中每个对象都包含着一个键和与其关联的值,然后再使用 getKey()
和 getValue()
将其分开。
2.1 随机数流
Ramdom
类在 Java 8 引入了流,有一组可以生成流的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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()); } }
为消除冗余代码,上面的示例创建了泛型 方法 show(Stream<T> stream)
,这个特性在之后会讲。类型参数 T
可以是任何东西,所以使用 Integer
、Long
和 Double
都可以。然而,Random
类只会生成 int
、long
和 double
等基本类型的值。幸运的是,boxed()
流操作会自动将基本类型转换为其对应的包装器类型,使得 show()
能够接受这个流。
2.2 整型的区间范围
IntStream
类提供了一个 range()
方法,可以生成一个流(由 int
值组成的序列),在编写循环时非常方便:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package stream;import java.util.stream.Collectors;import java.util.stream.IntStream;public class IntStreamRange { public static void main (String[] args) { for (int i: IntStream.range(1 , 5 ).toArray()) System.out.print(i + " " ); System.out.println(); System.out.println(IntStream.range(1 , 5 ).boxed().map(Object::toString).collect(Collectors.joining(" " ))); IntStream.range(1 , 5 ).boxed().forEach(x -> System.out.print(x + " " )); System.out.println(); } }
现在我们编写一个 repeat()
工具函数取代简单的 for
循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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); } }
2.3 Stream.generate()
Stream.generate()
可以接受任何的 Supplier<T>
(java.util.function
中的接口),并生成一个由 T
类型的对象组成的流。如果想创建一个由完全相同的对象组成的流,只需要将一个生成这些对象的 Lambda 表达式传给 generate()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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); Stream.generate(() -> "AsanoSaki" ).limit(2 ).forEach(s -> System.out.print(s + " " )); } }
2.4 Stream.iterate()
Stream.iterate()
从一个种子开始(第一个参数),然后将其传给第二个参数所引用的方法,其结果被添加到这个流上,并且保存下来作为下一次 iterate()
调用的第一个参数,以此类推。我们可以通过迭代生成一个之前实现过的斐波那契数列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 + " " )); } }
iterate()
只会记住结果,所以必须使用 x
来记住另一个元素。我们使用了 skip()
操作,这个之前没有介绍过,它会直接丢弃由其参数指定的相应数目的流元素,这里丢弃了前10个。
2.5 流生成器
在生成器 (Builder)设计模式中,我们创建一个生成器对象,为它提供多段构造信息,最后执行生成 (build)动作。 Stream
库提供了这样一个 Builder
,假设我们实现读取文件的每一行并将其转换为单词流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 + " " )); } }
其中的文本内容如下:
1 2 3 Hello world! Today is Saturday? Have a nice day!
构造器添加了文件中的所有单词,但是它没有调用 build()
,这意味着只要不调用 build()
就可以继续向 Builder
对象中添加单词。如果在调用 build()
之后还尝试向 Stream.Builder
中添加单词,则会产生异常。
2.6 Arrays
Arrays
类中包含了名为 stream()
的静态方法,可以将数组转换为流。可以是任何对象数组,也可以是 int
、long
和 double
基本类型,生成 IntStream
、LongStream
和 DoubleStream
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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(); } }
最后一次调用 stream()
时使用了两个额外的参数,第一个表示从数组的哪个位置开始选择元素,第二个表示停止位置(开区间),即在本例中选择数组中 [1, 4)
的元素。
3. 中间操作
3.1 对流元素进行排序
使用 sorted()
可以进行默认排序(从小到大),该方法也可以接受一个 Comparator
参数,该参数可以传入一个 Lambda 表达式也可以使用预定义好的 Comparator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 移除元素
distinct()
:移除流中的重复元素。
filter(Predicate)
:过滤只保留符合特定条件的元素,即满足过滤函数 Predicate
为 true
的流元素。
来看一个筛选素数的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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(); } }
rangeClosed()
包含了上界值,如果流中的元素没有任何一个取余操作的结果为0,则 noneMatch()
操作返回 true
,如果有任何一个计算结果等于0,则返回 false
。noneMatch()
会在第一次失败后退出,而不会把后面的所有计算都尝试一遍。
4. Optional类型
在研究终结操作之前,我们必须考虑一个问题:如果我们向流中请求对象,但是流中什么都没有,这时会发生什么呢?有没有某种我们可以使用的对象,既可以作为流元素来占位,也可以在我们要找的元素不存在时友好地告知我们(也就是说,不会抛出异常)。
这个想法被实现为 Optional
类型,某些标准的流操作会返回 Optional
对象,因为它们不能确保所要的结果一定存在,这些流操作列举如下:
findFirst()
:返回包含第一个元素的 Optional
。如果这个流为空,则返回 Optional.empty
。
findAny()
:返回包含任何元素的 Optional
。如果这个流为空,则返回 Optional.empty
。
max()
和 min()
分别返回包含流中最大值或最小值的 Optional
,如果这个流为空,则返回 Optional.empty
。
reduce()
的一个版本,它并不以一个 identity
对象作为其第一个参数(在 reduce()
的其他版本中,identity
对象会成为默认结果,所以不会有结果为空的风险),它会将返回值包在一个 Optional
中。
对于数值化的流 IntStream
、LongStream
和 DoubleStream
,average()
操作将其结果包在一个 Optional
中,以防流为空的情况。
看一下以下代码样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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
对象。Optional
有一个 toString()
方法,可以显示有用的信息。
注意,空流是通过 Stream.<String>empty()
创建的,如果只用了 Stream.empty()
而没有任何上下文信息,那么 Java 不知道它应该是什么类型的,而这种语法解决了该问题。如果编译器有足够的上下文信息,那么它可以推断出 empty()
调用的类型,就像下面这样:
1 Stream<String> s = Stream.empty();
我们接收到一个 Optional
时,首先要调用 isPresent()
,看看里面是不是有东西,如果有,再使用 get()
来获取。
4.1 便捷函数
有很多便捷函数,可用于获取 Optional
中的数据,它们简化了上面先检查再处理所包含对象的过程:
ifPresent(Consumer)
:如果对象存在,则用这个对象来调用 Consumer
,否则什么都不做。
orElse(otherObject)
:如果对象存在,则返回这个对象,否则返回 otherObject
。
orElseGet(Supplier)
:如果对象存在,则返回这个对象,否则返回使用 Supplier
函数创建的替代对象 。
orElseThrow(Supplier)
:如果对象存在,则返回这个对象,否则抛出一个使用 Supplier
函数创建的异常 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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" ))); System.out.println(emptyOptional.orElseThrow(() -> new Exception ("Supplier Exception" ))); } catch (Exception e) { System.out.println("Caught" + e); } } }
4.2 创建Optional
当需要自己编写生成 Optional
的代码时,有如下三种可以使用的静态方法:
empty()
:返回一个空的 Optional
。
of(value)
:如果已经知道这个 value
不为 null
,可以使用该方法将其包在一个 Optional
中。
ofNullable(value)
:如果不知道这个 value
是不是 null
,可以使用这个方法,如果 value
为 null
,它会自动返回 Optional.empty
,否则会将这个 value
包在一个 Optional
中。
来看一下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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" )); } }
4.3 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
的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 ) ); } }
尽管输出看上去像是一个流,其实每次进入 for
循环,它都会重新获得一个流,并跳过用 for
循环的索引设置的元素数,这就使其看上去像流中的连续元素,然后它执行 findFirst()
,获得剩余元素的中的第一个,它会被包在一个 Optional
中返回。
注意,我们的 for
循环是循环到 i == elements.length
,因此最后一个元素会超出这个流。不过这会自动变为 Optional.empty
。
4.4 由Optional组成的流
假设有一个可能会生成 null
值的生成器,如果使用这个生成器创建了一个流,我们自然想将这些元素包在 Optional
中,当使用这个流时,我们必须弄清楚如何获得 Optional
中的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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); } }
这里我使用了 只保留非 empty
的 Optional
,然后通过 map()
调用 get()
来获得包在其中的对象,因为每种情况都需要我们来决定“没有值”的含义,所以我们通常需要针对每种应用采取不同的方法。
5. 终结操作
这些操作接受一个流,并生成一个最终结果,它们不会再把任何东西发给某个后端的流。因此,终结操作总是我们在一个管线内可以做的最后一件事。
5.1 将流转换为一个数组
toArray()
:将流元素转换到适当类型的数组中。
toArray(generator)
:generator
用于在特定情况下分配自己的数组存储。
直接看样例:
1 2 3 4 5 6 7 8 9 10 11 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)); } }
5.2 在每个流元素上应用某个终结操作
forEach(Consumer)
:这种用法我们已经看到过很多次了,即以 System.out::println
作为 Consumer
函数。
forEachOrdered(Consumer)
:这个版本确保 forEach
对元素的操作顺序是原始的流的顺序。
第一种形式被明确地设计为可以以任何顺序操作元素,这只有在引入 parallel()
操作时才有意义。parallel()
让 Java 尝试在多个处理器上执行操作。它可以做到这一点,正是因为使用了流,它可以将流分割为多个流(通常情况是每个处理器一个流),并在不同的处理器上运行每个流。
我们在以下示例中引入 parallel()
来了解 forEachOrdered()
的作用和必要性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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(); } }
在第一个流中,我们没有使用 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
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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); } }
其中 FileToWordsBuilder.txt
文件内容如下:
1 2 3 4 Hello world! Today is Saturday? Have a nice day! This is digit 666.
Files.lines()
打开 Path
所指向的文件,并将其变为由文本行组成的 Stream
。它的下一行代码以一个或多个非单词字符(\\W+
)为边界来分割这些文本行,这里生成的数组通过 Arrays.stream()
变为 Stream
,然后其结果又被展开映射回一个由单词组成的 Stream
。matches(\\d+)
会找到并删除全是数字的 String
。接下来使用 String.trim()
去除周围可能存在的任何空白,最后把这些单词放到一个 TreeSet
中。
5.4 组合所有的流元素
reduce(BinaryOperator)
:使用 BinaryOperator
来组合所有的流元素,因为这个流可能为空,所以返回的是一个 Optional
。
reduce(identity, BinaryOperator)
:和上面一样,但是将 identity
用作这个组合的初始值,因此,即使这个流是空的,我们仍然能得到 identity
作为结果。
reduce(identity, BiFunction, BinaryOperator)
:这个更复杂(所以我们不会介绍),但是之所以把它列在这里,是因为它可能更高效。可以通过组合显式的 map()
和 reduce()
操作来更简单地表达这种需求。
来看一下最简单的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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); } }
我们在使用 reduce()
时,没有提供作为初始值 的第一个参数,这意味着它会生成一个 Optional
,只有当结果不是 empty
时,Optional.ifPresent()
方法才会调用 Consumer<Apple>
(之所以 System.out::println
能够符合,是因为它可以通过 toString()
方法将 Apple
转化为一个 String
)。
Lambda 表达式中的第一个参数 a0
是上次调用这个 reduce()
时带回的结果,第二个参数 a1
是来自流中的新值。如果 a0
的 price
大于60就接受 a0
,否则就接受 a1
,也就是序列中的下一个元素。
作为结果,我们得到的是流中第一个 price
大于60的 Apple
,一旦找到了一个这样的对象,它就会抓住不放,哪怕还会出现其他候选。