从迭代到Stream操作
以前,我们要处理一个集合,一般都是迭代它的元素并对每个元素做一些操作。例如,你想计算一本书中长词的数量,首先,把书的内容放入的一行为单位放入list:
1 | String content = new String(Files.readAllBytes( |
然后开始迭代:
1 | int count=0; |
这有啥问题吗?基本没有,除了这个代码很难并行。这就是Java8引入Stream的原因。同样的操作,Java8可以这么写:
1 | long count = words.stream().filter(w -> w.length() > 12).count(); |
stream
方法生成一个words
列表的Stream,filter
方法返回另一个只包含长度大于12的单词的Stream,count
方法把Stream归并为一个结果:Stream的元素个数。
Stream在外表是和集合很相似,它允许你变换和抽取数据。但它们其实是非常不同的:
- Stream并不存储它的元素,它们可能会存储在底层的集合中或者根据需要生成。
- Stream并不改变它们的数据源,相反,它们返回持有结果的新的Stream。
- Stream操作可能是延迟执行的。这意味着它们会等到需要结果的时候才会执行。例如,如果你只需要前五个长单词,那么
filter
方法会在第五次匹配之后停止过滤。因此,你甚至可以拥有无限的流。
很多人都觉得Stream表达式比循环更易读,此外,它们更容易并行。下面是如何并行地统计长词:
1 | long count=words.parallelStream().filter(w->w.length()>12).count(); |
只需要把stream
方法改成parallelStream
方法,就可以让StreamAPI并行地执行过滤和计数操作。
Stream遵从“做什么,而不是怎么做”这个原则。在上面的例子中,我们描述了需要做什么:获取长单词并统计。我们并不指明按什么顺序或在哪个线程中做。相反,循环一开始就指定了计算如何进行,因此失去了任何优化的机会。
当你使用Stream时,你会通过三个步骤来建立一个操作流水线:
- 创建一个Stream
- 在一个或多个步骤中,指定将初始的Stream转换成另一个Stream的中间操作
- 使用一个终止操作产生一个结果。这个操作强制它之前的延迟操作立即执行,在这之后,Stream不能再被使用了。
在我们例子中,Stream由stream
或parallelStream
方法产生。filter
方法对它进行转换,count
是终结操作。
Stream操作不会按照元素的调用顺序执行。在上面的例子中,只有在调用count
的时候才会执行Stream操作。当count
需要第一个元素的时候,filter
方法开始请求元素,直到它找到一个长度大于12的元素。
创建Stream
我们已经知道可以使用Java8加入Collection
接口的stream
方法把任何集合转变成Stream。如果你想要把数组转成Stream,可以使用静态方法Stream.of
:
1 | Stream<String> words = Stream.of(content.split("[\\P{L}]+")); |
of
有一个可变的参数,所以你可以用任意数量的参数来构造Stream:
1 | Stream<String> songs = Stream.of("gently", "down", "the", "stream"); |
使用Arrays.stream(array,from,to)
来使数组的一部分变成Stream。
要创建一个空的Stream,可以使用Stream.empty
这个静态方法:
1 | Stream<String> silence = Stream.empty(); |
Stream
接口有两个静态方法用来创建无限Stream。generate
方法接受一个无参数的函数(或者,技术上说,一个Supplier<T>接口的对象
)。当你需要一个Stream值时,这个函数会被调用来生成一个值。你可以使用下面的代码得到一个含有常量的Stream:
1 | Stream<String> echos = Stream.generate(() -> "Echo"); |
或者一个随机数的Stream:
1 | Stream<Double> randoms = Stream.generate(Math::random); |
而要产生像”0 1 2 3 …”这样的序列,使用iterate
方法。它接受一个种子(seed)值和一个函数(技术上来说,一个UnaryOperation<T>
对象),并且会对之前的值重复的应用该函数。例如:
1 | Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, bigInteger -> bigInteger.add(BigInteger.ONE)); |
序列的第一个值是seed,第二个值是f(seed)
,第三个是f(f(seed))
,以此类推。
Java的API中加入了不少生成Stream的方法。例如,Pattern
类现在有一个方法splitAsStream
可以用一个正则表达式切分CharSequence
对象,返回一个Stream。这是例子:
1 | Stream<String> splitStream = Pattern.compile("[\\P{L}]+").splitAsStream(content); |
静态方法Files.lines
返回包含文件所有的行的Stream。Stream
接口有一个AutoCloseable
父接口。当Stream
的close
方法被调用的时候,底层的文件也会被关闭。为了确保close
会被调用,可以使用Java7的try-with-resource声明:
1 | try(Stream<String> lines=Files.lines(Paths.get("gradlew"))) { |
filter,map和flatMap方法
流转换是从一个流读取数据,然后把转换后的数据放入另一个流中。我们已经看到filter
这个转换方法,它会生成一个新的流,其中包含了满足特定条件的所有元素。现在,我们要讲一个字符串流转换到另一个只包含长单词的流:
1 | List<String> wordsList = new ArrayList<>(); |
filter
的参数是一个Predicate<T>
,这是一个从T
到boolean
的函数。
我们通常会想对一个流中的值进行某种形式的转换。可以使用map
方法,传递给它一个执行转换的函数。例如,你可以用下面的代码把所以的单词转换为小写形式:
1 | Stream<String> lowcaseWords = words.map(String::toLowerCase); |
这里我们使用的是一个方法引用。但通常,我们会使用一个lambda表达式:
1 | Stream<Character> firstChars = words.map(s -> s.charAt(0)); |
当你使用map
的时候,会对每一个元素应用函数,并将返回值收集到一个新的流中。现在假设返回的不是一个值,而是包含多个值的流,如下所示:
1 | public static Stream<Character> characterStream(String s){ |
例如,characterStream("boat")
返回的是流stream['b','o','a','t']
。假设你这个方法映射到字符串流上:
1 | Stream<Stream<Character>> result = words.map(TransformMethod::characterStream); |
你会得到一个元素是流的流。要将它展开为一个只包含字符的流,你需要使用flatMap
方法而不是map
方法:
1 | Stream<Character> characterStream = words.flatMap(TransformMethod::characterStream); |
抽取子流和组合流
调用Stream
的limit(n)
方法,返回含有n个元素的新的流(或者返回原来的流如果原来流的大小比n小)。这个方法对于把无限的流切成一定大小是很有用的。比如:
1 | Stream<Double> randoms = Stream.generate(Math::random).limit(100); |
Stream的skip(n)
方法正好相反,它会丢弃前n个元素。在我们之前的读书的例子中,由于split
方法的作用方式,产生的第一个元素是一个空字符串(其实我没觉得第一个元素会是空格…),我们可以使用skip
方法来略过这个元素:
1 | Stream<String> words = Stream.of("Hello World".split("[\\P{L}]+")).skip(1); |
你可以用Stream
类的静态方法concat
把两个流连接起来:
1 | Stream<Character> combined = Stream.concat(characterStream("Hello"), characterStream("World")); |
当然,第一个流的长度不能是无限的,否则第二个流就永远没有机会被使用。
还有一个peek
方法,生成的流中的元素和原来的流完全一样,但是每次在其中取一个元素都会调用一个函数,这个方法对于debug是很方便。
1 | Object[] powers = Stream.iterate(1, integer -> integer * 2).peek(e -> System.out.println("Fetching:" + e)).limit(20).toArray(); |
当某个元素真正被访问了,才会打印出来一个消息。这样的方法可以验证对流的处理是lazy的。
有状态的转换
之前说的那些转换方法都是无状态的,当你从一个已过滤的或已映射过的流中获取一个元素的时候,它的结果并不依赖于之前获取的元素。除此之前,也有一些有状态的转换方法。例如distinct
方法,它根据原来的流中的元素返回一个具有相同属性,剔除了重复的元素的流。这个流显然是必须记住已经读取的元素的。
1 | Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct(); |
sorted
方法必须遍历整个流,并在产生任何元素之前就对其进行排序,毕竟,最小的元素可以位于最后。显然,你不能对一个无限的流进行排序:
1 | Stream<String> longestFirst=words.sorted(Comparator.comparingInt(String::length).reversed()); |
当然,你不需要使用流也可以对集合进行排序,当排序只是多个流操作之一时,sorted
方法是很有用的。
注意,Collection.sort
是在原来的集合中进行排序,而sorted
方法会返回一个已经排好序的新的流。
简单的聚合方法
我们已经了解了如何创建和转换流,现在介绍最重要的一点:如何从流获取答案。这一节中涉及的方法统称为聚合方法。它们把流聚合成一个值以便使用。聚合方法是终结操作。
之前我们已经看到过一个简单的聚合方法count
,他返回流的元素数量。其他的一些简单的聚合方法有min
和max
,但需要注意的是,它们返回的是一个Optional<T>
,它可能会封装了返回值,也可能表示没有返回值(如果流是空的)。在Java8之前,这种情况通常会返回null,但这可能会导致空指针异常。Java8中,Optional
类型是一种更好的表明没有返回值的方式。下一章中会详细讨论Optional
类型。下面的例子演示了如何从流中获取最大值:
1 | Optional<String> largest = sortedWords.max(String::compareToIgnoreCase); |
finFirst
方法返回集合中的第一个值,返回的也是Optional<T>
。它通常和filter
方法结合使用。比如,返回第一个以Q开头的单词:
1 | Optional<String> startWithQ = words.filter(s -> s.startsWith("Q")).findFirst(); |
如果你不在乎是否是第一个,只想要任意的匹配项,可以使用findAny
,它在你并行地处理流的时候非常高效,因为在任意片段中找到了第一个匹配项就会结束整个计算了。
1 | Optional<String> startWithQ = words.parallel().filter(s -> s.startsWith("Q")).findAny(); |
如果你只想知道流中是否有匹配的元素,使用anyMatch
。
1 | boolean aWordStartsWithQ = words.parallel().anyMatch(s -> s.startsWith("Q")); |
还有两个方法allMatch
和noneMatch
,它们分别在所有元素或没有元素匹配的时候返回true。虽然它们都会检查整个流,但你还是可以并行执行来提高效率。
Optional类型
一个Optional<T>
对象是一个T类型对象的封装,或者表示没有任何对象。Java8引入它是想把它作为一个相比T类型引用更安全的选择。但是,你只有正确的使用它,它才是更安全的。
它的get
方法会返回封装的元素如果存在的话,否则会抛出异常NoSuchElementException
。因此,代码
1 | Optional<T> optionalValue = ...; |
并不比下面的代码更安全:
1 | T value = ...; |
在前面的章节中,我们看到isPresent
方法会反映Optional<T>
对象中是否有值,但是,代码
1 | if (optionalValue.isPresent()) optionalValue.get().someMethod(); |
并不比下面的代码更简洁
1 | if (value != null) value.someMethod(); |
使用Optional值
高效的使用Optional的关键是,使用一个或者接收正确的值或者产生替代值的方法。
方法ifPresent
接收一个函数,如果存在可选值,那么它会被传递给函数,如果没有,什么都不会发生。
1 | optionalValue.ifPresent(System.out::println); |
ifPresent
不会返回值,如果你希望对结果进行处理,用map
方法:
1 | Optional<Boolean> added = optionalValue.map(results::add); |
added
可能有三种值:封装着true或false的Optional
对象如果optionalValue
中有值的话,否则的话是一个空的Optional
对象。
这个map
方法和流的map
方法是类似的,你可以把Optional
看成是一个大小是0或1的流,返回的流的大小也是0或1,后一种情况会应用map
接收的函数。
我们已经知道当一个可选值存在时应该如何优雅的对它进行处理。另一个使用可选值的策略是当没有值存在时产生一个替代值。通常,当没有匹配项时,你会想要使用一个默认值,例如空字符串。
1 | String result = optionalString.orElse(""); |
你也可以调用函数来计算默认值
1 | String result=optionalString.orElseGet(() -> System.getProperty("user.dir")) |
或者,你想在没有值的时候抛出其他的异常
1 | String error = optionalString.orElseThrow(NoSuchElementException::new); |
生成Optional值
如果你想编写一个产生Optional
对象的方法,有一些静态方法可以选择。例如,Optional.of
和Optional.empty
方法。
1 | public static Optional<Double> inverse(Double x){ |
ofNullable
方法计划作为null值和可选值之间的桥梁。Optional.ofNullable(obj)
在obj不是null的时候返回Optional.of(obj)
,否则返回Optional.empty()
。
使用flatMap组合可选值的函数
假设你一个方法f
可以生成Optional<T>
,T类型又有一个方法g
可以生成Optional<U>
。如果它们是一般的方法,你可以用s.f().g()
把它们组合起来。但是,这样的组合在这里行不通,因为s.f()
的类型是Optional<T>
,而不是T。但你可以这样写:
1 | Optional<U> = s.f().flatMap(T::g); |
如果s.f()
存在值,那么会继续调用g
,否则,会返回一个空的Optional<U>
。
显然,如果你有更多的返回Optional
值的方法或lambda表达式,你可以重复这个步骤,通过不断调用flatMap
,创建一个调用的流水线,只有当其中的每部分都成功时,整个处理才算是成功了。
以上面的安全的求倒数的方法为例,我们再写一个安全的求平方根的方法:
1 | public static Optional<Double> squre(Double x){ |
那么你可以这样计算倒数的平方根:
1 | Optional<Double> result = inverse(4.0).flatMap(OptionalType::squre); |
或者这样:
1 | Optional<Double> result = Optional.of(4.0).flatMap(OptionalType::inverse).flatMap(OptionalType::inverse); |
不管是inverse
还是square
方法返回Optional.empty()
,结果都为空。
和map
一样,其实flatMap
和流的flatMap
的方法很类似,如果你把Optional
看做一个大小是0或1的流。