zjjfly's blog

Java,Clojure,Scala...

0%

快学Java8 第二章上

从迭代到Stream操作

以前,我们要处理一个集合,一般都是迭代它的元素并对每个元素做一些操作。例如,你想计算一本书中长词的数量,首先,把书的内容放入的一行为单位放入list:

1
2
3
String content = new String(Files.readAllBytes(
Paths.get("alice.txt")), StandardCharse.UTF_8);
List<String> words = Arrays.asList(content.split("[\\P{L}]+"));

然后开始迭代:

1
2
3
4
5
6
int count=0;
for (String word : words) {
if (word.length()>12){
count++;
}
}

这有啥问题吗?基本没有,除了这个代码很难并行。这就是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时,你会通过三个步骤来建立一个操作流水线:

  1. 创建一个Stream
  2. 在一个或多个步骤中,指定将初始的Stream转换成另一个Stream的中间操作
  3. 使用一个终止操作产生一个结果。这个操作强制它之前的延迟操作立即执行,在这之后,Stream不能再被使用了。

在我们例子中,Stream由streamparallelStream方法产生。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父接口。当Streamclose方法被调用的时候,底层的文件也会被关闭。为了确保close会被调用,可以使用Java7的try-with-resource声明:

1
2
3
4
5
try(Stream<String> lines=Files.lines(Paths.get("gradlew"))) {
System.out.println(lines.count());
} catch (IOException e) {
e.printStackTrace();
}

filter,map和flatMap方法

流转换是从一个流读取数据,然后把转换后的数据放入另一个流中。我们已经看到filter这个转换方法,它会生成一个新的流,其中包含了满足特定条件的所有元素。现在,我们要讲一个字符串流转换到另一个只包含长单词的流:

1
2
3
List<String> wordsList = new ArrayList<>();
Stream<String> words = wordsList.stream();
Stream<String> longWords = words.filter(s -> s.length() > 12);

filter的参数是一个Predicate<T>,这是一个从Tboolean的函数。
我们通常会想对一个流中的值进行某种形式的转换。可以使用map方法,传递给它一个执行转换的函数。例如,你可以用下面的代码把所以的单词转换为小写形式:

1
Stream<String> lowcaseWords = words.map(String::toLowerCase);

这里我们使用的是一个方法引用。但通常,我们会使用一个lambda表达式:

1
Stream<Character> firstChars = words.map(s -> s.charAt(0));

当你使用map的时候,会对每一个元素应用函数,并将返回值收集到一个新的流中。现在假设返回的不是一个值,而是包含多个值的流,如下所示:

1
2
3
4
5
6
7
public static Stream<Character> characterStream(String s){
List<Character> result = new ArrayList<>();
for (char c : s.toCharArray()) {
result.add(c);
}
return result.stream();
}

例如,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);

抽取子流和组合流

调用Streamlimit(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,他返回流的元素数量。其他的一些简单的聚合方法有minmax,但需要注意的是,它们返回的是一个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"));

还有两个方法allMatchnoneMatch,它们分别在所有元素或没有元素匹配的时候返回true。虽然它们都会检查整个流,但你还是可以并行执行来提高效率。

Optional类型

一个Optional<T>对象是一个T类型对象的封装,或者表示没有任何对象。Java8引入它是想把它作为一个相比T类型引用更安全的选择。但是,你只有正确的使用它,它才是更安全的
它的get方法会返回封装的元素如果存在的话,否则会抛出异常NoSuchElementException。因此,代码

1
2
Optional<T> optionalValue = ...; 
optionalValue.get().someMethod()

并不比下面的代码更安全:

1
2
T value = ...; 
value.someMethod();

在前面的章节中,我们看到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.ofOptional.empty方法。

1
2
3
public static Optional<Double> inverse(Double x){
return x==0?Optional.empty():Optional.of(1/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
2
3
public static Optional<Double> squre(Double x){
return x==0?Optional.empty():Optional.of(Math.sqrt(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的流。