CONTENTS
1. Java8对流的支持
流是一个与任何特定的存储机制都没有关系的元素序列。不同于在集合中遍历元素,使用流的时候,我们是从一个管道中抽取元素,并对它们逬行操作。这些管道通常会被串联到一起,形成这个流上的一个操作管线。
流的一个核心优点是,它们能使我们的程序更小,也更好理解。当配合流使用时,Lambda 表达式和方法引用就发挥出其威力了。流大大提升了 Java 8 的吸引力。
假设我们想按照有序方式显示随机选择的5~20范围内的,不重复的整数,借助流我们只需要说明想做什么即可实现:
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);
/*
* 6
* 10
* 13
* 16
* 18
*/
}
}
我们先为 Random
对象设置一个随机种子,ints()
方法会生成一个流,该方法有多个重载版本,其中两个参数的版本可以设置所生成值的上下界。使用中间流操作 distinct()
去掉重复的值,再使用 limit()
选择前5个值,sorted()
表示元素是有序的,最后我们想显示每一个条目,所以使用了 forEach()
,它会根据我们传递的函数,在每个流对象上执行一个操作。这里我们传递了一个方法引用 System.out::println
,用于将每个条目显示在控制台上。
使用流实现的代码我们看不到任何显式的迭代机制,因此称为内部迭代,这是流编程的一个核心特性。
2. 流的创建
使用 Stream.of()
,可以轻松地将一组条目变成一个流:
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);
/*
* 4
* 1
* 7
* Hello World AsanoSaki
*/
}
}
此外,每个 Collection
都可以使用 stream()
方法来生成一个流:
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);
/*
* 6
* HELLO AND JAVA WORLD
* PHI: 1.618
* E: 2.718
* PI: 3.14
*/
}
}
在创建了一个 List<A>
之后,只需要调用一下 stream()
这个所有集合类都有的方法。中间的 map()
操作接受流中的毎个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去,这里的 mapToInt()
将一个对象流转变成了一个包含 Integer
的 IntStream
。对于 Float
和 Double
也有名字类似的操作。
collect()
操作会根据其参数将所有的流元素组合起来,当我们使用 Collectors.joining()
时,得到的结果是一个 String
,每个元素都会以 joining()
的参数分隔,还有其他很多 Collectors
,可以生成不同的结果。
为了从 Map
集合生成一个流,我们首先调用 entrySet()
来生成一个对象流,其中每个对象都包含着一个键和与其关联的值,然后再使用 getKey()
和 getValue()
将其分开。
2.1 随机数流
Ramdom
类在 Java 8 引入了流,有一组可以生成流的方法:
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()); // 设置流的大小与上下边界
/*
* -1172028779 1717241110 -2014573909
* 0.053412216308810656 0.5779976127815049 0.4170137422770571
* 17 18 18
* 1122537102 491149179
* 19 18
*/
}
}
为消除冗余代码,上面的示例创建了泛型方法 show(Stream<T> stream)
,这个特性在之后会讲。类型参数 T
可以是任何东西,所以使用 Integer
、Long
和 Double
都可以。然而,Random
类只会生成 int
、long
和 double
等基本类型的值。幸运的是,boxed()
流操作会自动将基本类型转换为其对应的包装器类型,使得 show()
能够接受这个流。
2.2 整型的区间范围
IntStream
类提供了一个 range()
方法,可以生成一个流(由 int
值组成的序列),在编写循环时非常方便:
package stream;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class IntStreamRange {
public static void main(String[] args) {
// for-in循环
for (int i: IntStream.range(1, 5).toArray())
System.out.print(i + " "); // 1 2 3 4
System.out.println();
// Stream
System.out.println(IntStream.range(1, 5).boxed().map(Object::toString).collect(Collectors.joining(" "))); // 1 2 3 4
IntStream.range(1, 5).boxed().forEach(x -> System.out.print(x + " ")); // 1 2 3 4
System.out.println();
}
}
现在我们编写一个 repeat()
工具函数取代简单的 for
循环:
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);
/*
* Lambda
* Lambda
* Lambda
* Hello
* Hello
*/
}
}
2.3 Stream.generate()
Stream.generate()
可以接受任何的 Supplier<T>
(java.util.function
中的接口),并生成一个由 T
类型的对象组成的流。如果想创建一个由完全相同的对象组成的流,只需要将一个生成这些对象的 Lambda 表达式传给 generate()
:
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); // YNZBRNYGCF
Stream.generate(() -> "AsanoSaki").limit(2).forEach(s -> System.out.print(s + " ")); // AsanoSaki AsanoSaki
}
}
2.4 Stream.iterate()
Stream.iterate()
从一个种子开始(第一个参数),然后将其传给第二个参数所引用的方法,其结果被添加到这个流上,并且保存下来作为下一次 iterate()
调用的第一个参数,以此类推。我们可以通过迭代生成一个之前实现过的斐波那契数列:
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 + " "));
/*
* 0 1 1 2 3 5 8 13 21 34
* 55 89 144 233 377 610 987 1597 2584 4181
*/
}
}
iterate()
只会记住结果,所以必须使用 x
来记住另一个元素。我们使用了 skip()
操作,这个之前没有介绍过,它会直接丢弃由其参数指定的相应数目的流元素,这里丢弃了前10个。
2.5 流生成器
在生成器(Builder)设计模式中,我们创建一个生成器对象,为它提供多段构造信息,最后执行生成(build)动作。 Stream
库提供了这样一个 Builder
,假设我们实现读取文件的每一行并将其转换为单词流:
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 + " "));
/*
* Hello world Today is Saturday Have a nice day
*/
}
}
其中的文本内容如下:
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
:
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();
/*
* ABC XYZ YYJ
* 1 2 3
* 3.14 2.72 1.62
* 8 7 6
*/
}
}
最后一次调用 stream()
时使用了两个额外的参数,第一个表示从数组的哪个位置开始选择元素,第二个表示停止位置(开区间),即在本例中选择数组中 [1, 4)
的元素。
3. 中间操作
3.1 对流元素进行排序
使用 sorted()
可以进行默认排序(从小到大),该方法也可以接受一个 Comparator
参数,该参数可以传入一个 Lambda 表达式也可以使用预定义好的 Comparator
:
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 1
*/
}
}
3.2 移除元素
distinct()
:移除流中的重复元素。filter(Predicate)
:过滤只保留符合特定条件的元素,即满足过滤函数Predicate
为true
的流元素。
来看一个筛选素数的例子:
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();
/*
* 2 3 5 7 11 13 17 19 23 29
*/
}
}
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
中,以防流为空的情况。
看一下以下代码样例:
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
* Hello
* Nothing in Optional!
*/
}
}
这时不会因为流是空的而抛出异常,而是会得到一个 Optional.empty
对象。Optional
有一个 toString()
方法,可以显示有用的信息。
注意,空流是通过 Stream.<String>empty()
创建的,如果只用了 Stream.empty()
而没有任何上下文信息,那么 Java 不知道它应该是什么类型的,而这种语法解决了该问题。如果编译器有足够的上下文信息,那么它可以推断出 empty()
调用的类型,就像下面这样:
Stream<String> s = Stream.empty();
我们接收到一个 Optional
时,首先要调用 isPresent()
,看看里面是不是有东西,如果有,再使用 get()
来获取。
4.1 便捷函数
有很多便捷函数,可用于获取 Optional
中的数据,它们简化了上面先检查再处理所包含对象的过程:
ifPresent(Consumer)
:如果对象存在,则用这个对象来调用Consumer
,否则什么都不做。orElse(otherObject)
:如果对象存在,则返回这个对象,否则返回otherObject
。orElseGet(Supplier)
:如果对象存在,则返回这个对象,否则返回使用Supplier
函数创建的替代对象。orElseThrow(Supplier)
:如果对象存在,则返回这个对象,否则抛出一个使用Supplier
函数创建的异常。
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"))); // 先执行非空Optional
System.out.println(emptyOptional.orElseThrow(() -> new Exception("Supplier Exception")));
} catch (Exception e) {
System.out.println("Caught" + e);
}
/*
* ---------- ifPresent ----------
* Hello
* ---------- orElse ----------
* Other String
* Hello
* ---------- orElseGet ----------
* Other Supplier Object
* Hello
* ---------- orElseThrow ----------
* Hello
* Caughtjava.lang.Exception: Supplier Exception
*/
}
}
4.2 创建Optional
当需要自己编写生成 Optional
的代码时,有如下三种可以使用的静态方法:
empty()
:返回一个空的Optional
。of(value)
:如果已经知道这个value
不为null
,可以使用该方法将其包在一个Optional
中。ofNullable(value)
:如果不知道这个value
是不是null
,可以使用这个方法,如果value
为null
,它会自动返回Optional.empty
,否则会将这个value
包在一个Optional
中。
来看一下示例:
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"));
}
/*
* Hello
* Empty
* 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
的用法:
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)
);
/*
* Optional[Dog]
* Optional[Cat]
* Optional.empty
* Optional.empty
* Optional.empty
*/
}
}
尽管输出看上去像是一个流,其实每次进入 for
循环,它都会重新获得一个流,并跳过用 for
循环的索引设置的元素数,这就使其看上去像流中的连续元素,然后它执行 findFirst()
,获得剩余元素的中的第一个,它会被包在一个 Optional
中返回。
注意,我们的 for
循环是循环到 i == elements.length
,因此最后一个元素会超出这个流。不过这会自动变为 Optional.empty
。
4.4 由Optional组成的流
假设有一个可能会生成 null
值的生成器,如果使用这个生成器创建了一个流,我们自然想将这些元素包在 Optional
中,当使用这个流时,我们必须弄清楚如何获得 Optional
中的对象:
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);
/*
* Optional[Signal(Case 2)]
* Optional[Signal(Case 1)]
* Optional[Signal(Case 2)]
* Optional.empty
* Optional.empty
* --------------------
* Signal(Case 2)
* Signal(Case 1)
* Signal(Case 2)
* Signal(Case 2)
*/
}
}
这里我使用了 只保留非 empty
的 Optional
,然后通过 map()
调用 get()
来获得包在其中的对象,因为每种情况都需要我们来决定“没有值”的含义,所以我们通常需要针对每种应用采取不同的方法。
5. 终结操作
这些操作接受一个流,并生成一个最终结果,它们不会再把任何东西发给某个后端的流。因此,终结操作总是我们在一个管线内可以做的最后一件事。
5.1 将流转换为一个数组
toArray()
:将流元素转换到适当类型的数组中。toArray(generator)
:generator
用于在特定情况下分配自己的数组存储。
直接看样例:
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)); // [8, 5, 3, 1, 1, 9, 8, 0, 2, 7]
}
}
5.2 在每个流元素上应用某个终结操作
forEach(Consumer)
:这种用法我们已经看到过很多次了,即以System.out::println
作为Consumer
函数。forEachOrdered(Consumer)
:这个版本确保forEach
对元素的操作顺序是原始的流的顺序。
第一种形式被明确地设计为可以以任何顺序操作元素,这只有在引入 parallel()
操作时才有意义。parallel()
让 Java 尝试在多个处理器上执行操作。它可以做到这一点,正是因为使用了流,它可以将流分割为多个流(通常情况是每个处理器一个流),并在不同的处理器上运行每个流。
我们在以下示例中引入 parallel()
来了解 forEachOrdered()
的作用和必要性:
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();
/*
* 8 5 3 1 1 9 8 0 2 7
* 8 9 0 7 2 3 5 1 8 1
* 8 5 3 1 1 9 8 0 2 7
*/
}
}
在第一个流中,我们没有使用 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
中:
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);
/*
* [Have, Hello, Saturday, This, Today, a, day, digit, is, nice, world]
*/
}
}
其中 FileToWordsBuilder.txt
文件内容如下:
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()
操作来更简单地表达这种需求。
来看一下最简单的用法:
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);
/*
* Peek: Apple(58)
* Peek: Apple(55)
* Peek: Apple(93)
* Peek: Apple(61)
* Peek: Apple(61)
* Apple(93)
*/
}
}
我们在使用 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
,一旦找到了一个这样的对象,它就会抓住不放,哪怕还会出现其他候选。