第十四章 流式编程
集合优化了对象的存储,而流(Streams)则是关于一组组对象的处理。
流(Streams)是与任何特定存储机制无关的元素序列——实际上,我们说流是 "没有存储 "的。
其实流还是有存储的,不然一些功能都不知道怎么实现,比如distinct功能。
流的一个核心好处是,它使得程序更加短小并且更易理解。当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体。流使得 Java 8 更具吸引力。
但是debug很不方便
// streams/Randoms.java
import java.util.*;
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct()
.limit(7)
.sorted()
.forEach(System.out::println);
}
}
输出:
6
10
13
16
17
18
19
注意了如果limit参数比较大接近ints规定的范围,那么在distinct1就会卡主,亲测。
上面就是一个随机流,根据种子和限定范围产生随机数,然后去重,排序,遍历打印出来。
对比一下声明式编程:
// streams/ImperativeRandoms.java
import java.util.*;
public class ImperativeRandoms {
public static void main(String[] args) {
Random rand = new Random(47);
SortedSet<Integer> rints = new TreeSet<>();
while(rints.size() < 7) {
int r = rand.nextInt(20);
if(r < 5) continue;
rints.add(r);
}
System.out.println(rints);
}
}
是不是感觉流式编程很简洁。
流支持
主要说的是引入default
关键字,通过这种方案,设计者们可以将流式(stream)方法平滑地嵌入到现有类中。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是汇入一个集合)。
开发用了多了之后发现基本最后都是汇入一个集合里面。
流创建
// streams/StreamOf.java
import java.util.stream.*;
public class StreamOf {
public static void main(String[] args) {
Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
.forEach(System.out::println);
Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
.forEach(System.out::print);
System.out.println();
Stream.of(3.14159, 2.718, 1.618)
.forEach(System.out::println);
}
}
主要就是通过Stream.of
方法将一组对象数据转换成流。
输出结果:
Bubble(1)
Bubble(2)
Bubble(3)
It's a wonderful day for pie!
3.14159
2.718
1.618
// streams/CollectionToStream.java
import java.util.*;
import java.util.stream.*;
public class CollectionToStream {
public static void main(String[] args) {
List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
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);
m.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue())
.forEach(System.out::println);
}
}
集合通过Stream
来产生一个流,一般集合用流还是比较多的。
输出:
6
a pie! It's for wonderful day
phi: 1.618
e: 2.718
pi: 3.14159
随机数流
// streams/RandomGenerators.java
import java.util.*;
import java.util.stream.*;
public class RandomGenerators {
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());
// 控制上限和下限:
show(rand.ints(10, 20).boxed());
show(rand.longs(50, 100).boxed());
show(rand.doubles(20, 30).boxed());
// 控制流大小:
show(rand.ints(2).boxed());
show(rand.longs(2).boxed());
show(rand.doubles(2).boxed());
// 控制流的大小和界限
show(rand.ints(3, 3, 9).boxed());
show(rand.longs(3, 12, 22).boxed());
show(rand.doubles(3, 11.5, 12.3).boxed());
}
}
输出:
-1172028779
1717241110
-2014573909
229403722
++++++++
2955289354441303771
3476817843704654257
-8917117694134521474
4941259272818818752
++++++++
0.2613610344283964
0.0508673570556899
0.8037155449603999
0.7620665811558285
++++++++
16
10
11
12
++++++++
65
99
54
58
++++++++
29.86777681078574
24.83968447804611
20.09247112332014
24.046793846338723
++++++++
1169976606
1947946283
++++++++
2970202997824602425
-2325326920272830366
++++++++
0.7024254510631527
0.6648552384607359
++++++++
6
7
7
++++++++
17
12
20
++++++++
12.27872414236691
11.732085449736195
12.196509449817267
++++++++
boxed()
流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show()
能够接受流。Random
类只能生成基本类型,而流操作需要包装类所以可以通过boxed
进行转换。
我们可以使用 Random 为任意对象集合创建 Supplier。
从文本文件提供字符串对象的例子如下。
// streams/Cheese.dat
Not much of a cheese shop really, is it?
Finest in the district, sir.
And what leads you to that conclusion?
Well, it's so clean.
It's certainly uncontaminated by cheese.
// streams/RandomWords.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
import java.io.*;
import java.nio.file.*;
public class RandomWords implements Supplier<String> {
List<String> words = new ArrayList<>();
Random rand = new Random(47);
RandomWords(String fname) throws IOException {
List<String> lines = Files.readAllLines(Paths.get(fname));
// 略过第一行
for (String line : lines.subList(1, lines.size())) {
for (String word : line.split("[ .?,]+"))
words.add(word.toLowerCase());
}
}
public String get() {
return words.get(rand.nextInt(words.size()));
}
@Override
public String toString() {
return words.stream()
.collect(Collectors.joining(" "));
}
public static void main(String[] args) throws Exception {
System.out.println(
Stream.generate(new RandomWords("Cheese.dat"))
.limit(10)
.collect(Collectors.joining(" ")));
}
}
输出:
it shop sir the much cheese by conclusion district is
主要关注一下Stream.generate()
的用法,先看它的定义。
static Stream generate(Supplier<? extends T> s)
接收一个Supplier
对象,所以RandomWords
实现了Supplier
接口,实现了get
方法。在Stream.generate()
内部一个时调用get
方法获得传入的泛型对象的,从而生成流的。
int类型范围
// streams/Ranges.java
import static java.util.stream.IntStream.*;
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());
}
}
主要就是IntStream
类里面range()方法的使用,可以用它来做循环和python有点像。
输出:
145
145
145
实用小功能 repeat()
可以用来替换简单的 for
循环。代码示例:
// onjava/Repeat.java
package onjava;
import static java.util.stream.IntStream.*;
public class Repeat {
public static void repeat(int n, Runnable action) {
range(0, n).forEach(i -> action.run());
}
}
其产生的循环更加清晰:
// streams/Looping.java
import static onjava.Repeat.*;
public class Looping {
static void hi() {
System.out.println("Hi!");
}
public static void main(String[] args) {
repeat(3, () -> System.out.println("Looping!"));
repeat(2, Looping::hi);
}
}
输出:
Looping!
Looping!
Looping!
Hi!
Hi!
generate()
参照 RandomWords.java
中 Stream.generate()
搭配 Supplier<T>
使用的例子。代码示例:
// streams/Generator.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Generator implements Supplier<String> {
Random rand = new Random(47);
char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
public String get() {
return "" + letters[rand.nextInt(letters.length)];
}
public static void main(String[] args) {
String word = Stream.generate(new Generator())
.limit(30)
.collect(Collectors.joining());
System.out.println(word);
}
}
输出:
YNZBRNYGCFOWZNTCQRGSEGZMMJMROE
如果要创建包含相同对象的流,只需要传递一个生成那些对象的 lambda
到 generate()
中:
// streams/Duplicator.java
import java.util.stream.*;
public class Duplicator {
public static void main(String[] args) {
Stream.generate(() -> "duplicate")
.limit(3)
.forEach(System.out::println);
}
}
输出:
duplicate
duplicate
duplicate
如下是在本章之前例子中使用过的 Bubble
类。注意它包含了自己的静态生成器(Static generator)方法。
// streams/Bubble.java
import java.util.function.*;
public class Bubble {
public final int i;
public Bubble(int n) {
i = n;
}
@Override
public String toString() {
return "Bubble(" + i + ")";
}
private static int count = 0;
public static Bubble bubbler() {
return new Bubble(count++);
}
}
由于 bubbler()
与 Supplier<Bubble>
是接口兼容的,我们可以将其方法引用直接传递给 Stream.generate()
:
// streams/Bubbles.java
import java.util.stream.*;
public class Bubbles {
public static void main(String[] args) {
Stream.generate(Bubble::bubbler)
.limit(5)
.forEach(System.out::println);
}
}
输出:
Bubble(0)
Bubble(1)
Bubble(2)
Bubble(3)
Bubble(4)
这边解释一下这句话:
由于
bubbler()
与Supplier<Bubble>
是接口兼容的,我们可以将其方法引用直接传递给 Stream.generate()
:
之前在说函数式接口的时候当类里面只有一个静态方法的时候 ,他和函数式接口就是兼容的。
iterate()
Stream.iterate()
产生的流的第一个元素是种子(iterate方法的第一个参数),然后将种子传递给方法(iterate方法的第二个参数)。方法运行的结果被添加到流(作为流的下一个元素),并被存储起来,作为下次调用 iterate()
方法时的第一个参数,以此类推。我们可以利用 iterate()
生成一个斐波那契数列(上一章已经遇到过Fibonacci)。代码示例:
// streams/Fibonacci.java
import java.util.stream.*;
public class Fibonacci {
int x = 1;
Stream<Integer> numbers() {
return Stream.iterate(0, i -> {
int result = x + i;
x = i;
return result;
});
}
public static void main(String[] args) {
new Fibonacci().numbers()
.skip(20) // 过滤前 20 个
.limit(10) // 然后取 10 个
.forEach(System.out::println);
}
}
输出:
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
第一个参数就是参数i
后面生成的参数会继续反过来给参数i
。
流的建造者模式
在建造者模式(Builder design pattern)中,首先创建一个 builder
对象,然后将创建流所需的多个信息传递给它,最后builder
对象执行”创建“流的操作。
直接看代码:
// streams/FileToWordsBuilder.java
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
public class FileToWordsBuilder {
Stream.Builder<String> builder = Stream.builder();
public FileToWordsBuilder(String filePath) throws Exception {
Files.lines(Paths.get(filePath))
.skip(1) // 略过开头的注释行
.forEach(line -> {
for (String w : line.split("[ .?,]+"))
builder.add(w);
});
}
Stream<String> stream() {
return builder.build();
}
public static void main(String[] args) throws Exception {
new FileToWordsBuilder("Cheese.dat")
.stream()
.limit(7)
.map(w -> w + " ")
.forEach(System.out::print);
}
}
输出:
Not much of a cheese shop really
创建builder实例然后添加对象进去,最后调用build防范返回就好。
Arrays
// streams/Machine2.java
import java.util.*;
import onjava.Operations;
public class Machine2 {
public static void main(String[] args) {
Arrays.stream(new Operations[] {
() -> Operations.show("Bing"),
() -> Operations.show("Crack"),
() -> Operations.show("Twist"),
() -> Operations.show("Pop")
}).forEach(Operations::execute);
}
}
Arrays包含stream方法直接用就可以
输出:
Bing
Crack
Twist
Pop
new Operations[]
表达式动态创建了 Operations
对象的数组。
stream()
同样可以产生 IntStream,LongStream 和 DoubleStream。
// streams/ArrayStreams.java
import java.util.*;
import java.util.stream.*;
public class ArrayStreams {
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();
// 选择一个子域:
Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)
.forEach(n -> System.out.format("%d ", n));
}
}
输出:
3.141590 2.718000 1.618000
1 3 5
11 22 44 66
7 15 28
最后一次 stream()
的调用有两个额外的参数。第一个参数告诉 stream()
从数组的哪个位置开始选择元素,第二个参数用于告知在哪里停止。每种不同类型的 stream()
都有类似的操作。
正则表达式
Java 8 在 java.util.regex.Pattern
中增加了一个新的方法 splitAsStream()
。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence,因此不能将流作为 splitAsStream()
的参数。
// streams/FileToWordsRegexp.java
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
import java.util.regex.Pattern;
public class FileToWordsRegexp {
private String all;
public FileToWordsRegexp(String filePath) throws Exception {
all = Files.lines(Paths.get(filePath))
.skip(1) // First (comment) line
.collect(Collectors.joining(" "));
}
public Stream<String> stream() {
return Pattern
.compile("[ .,?]+").splitAsStream(all);
}
public static void
main(String[] args) throws Exception {
FileToWordsRegexp fw = new FileToWordsRegexp("Cheese.dat");
fw.stream()
.limit(7)
.map(w -> w + " ")
.forEach(System.out::print);
fw.stream()
.skip(7)
.limit(2)
.map(w -> w + " ")
.forEach(System.out::print);
}
}
输出:
3.141590 2.718000 1.618000
1 3 5
11 22 44 66
7 15 28
就是通过流和正则将文件转为单词流。
这里有个限制,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这丢掉了流操作非常重要的优势:
- “不需要把流存储起来。”当然,流确实需要一些内部存储,但存储的只是序列的一小部分,和存储整个序列不同。
- 它们是懒加载计算的。
如上所说流其实是需要存储的,还有就是流其实是懒加载的。但是上面的例子是把文件里面内容全部加进了内存里面。
中间操作
中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。
跟踪和调试
peek()
操作的目的是帮助调试。它允许你无修改地查看流中的元素。代码示例:
// streams/Peeking.java
class Peeking {
public static void main(String[] args) throws Exception {
FileToWords.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);
}
}
输出:
Well WELL well it IT it s S s so SO so
因为流其实不太容易调试,所以就用peek
函数来辅助调试。
流元素排序
// streams/SortedComparator.java
import java.util.*;
public class SortedComparator {
public static void main(String[] args) throws Exception {
FileToWords.stream("Cheese.dat")
.skip(10)
.limit(10)
.sorted(Comparator.reverseOrder())
.map(w -> w + " ")
.forEach(System.out::print);
}
}
输出:
you what to the that sir leads in district And
一般我们都是用sorted进行排序,但是在比较一些对象的时候显然不太合适。这时候就可以传入compactor
参数进行比较。这里默认的是翻转比较器。
移除元素
distinct()
:在Randoms.java
类中的distinct()
可用于消除流中的重复元素。相比创建一个 Set 集合来消除重复,该方法的工作量要少得多。filter(Predicate)
:过滤操作,保留如下元素:若元素传递给过滤函数产生的结果为true
。
在下例中,isPrime()
作为过滤函数,用于检测质数
// streams/Prime.java
import java.util.stream.*;
import static java.util.stream.LongStream.*;
public class Prime {
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));
}
}
输出:
2 3 5 7 11 13 17 19 23 29
467 479 487 491 499 503 509 521 523 541
rangeClosed()
包含了上限值。如果不能整除,即余数不等于 0,则 noneMatch()
操作返回 true
,如果出现任何等于 0 的结果则返回 false
。 noneMatch()
操作一旦有失败就会退出。
应用函数到元素
map(Function)
:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。mapToInt(ToIntFunction)
:操作同上,但结果是 IntStream。mapToLong(ToLongFunction)
:操作同上,但结果是 LongStream。mapToDouble(ToDoubleFunction)
:操作同上,但结果是 DoubleStream。
在这里,我们使用 map()
映射多种函数到一个字符串流中。代码示例:
// streams/FunctionMap.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class FunctionMap {
static String[] elements = { "12", "", "23", "45" };
static Stream<String>
testStream() {
return Arrays.stream(elements);
}
static void test(String descr, Function<String, String> func) {
System.out.println(" ---( " + descr + " )---");
testStream()
.map(func)
.forEach(System.out::println);
}
public static void main(String[] args) {
test("add brackets", s -> "[" + s + "]");
test("Increment", s -> {
try {
return Integer.parseInt(s) + 1 + "";
}
catch(NumberFormatException e) {
return s;
}
}
);
test("Replace", s -> s.replace("2", "9"));
test("Take last digit", s -> s.length() > 0 ?
s.charAt(s.length() - 1) + "" : s);
}
}
输出:
---( add brackets )---
[12]
[]
[23]
[45]
---( Increment )---
13
24
46
---( Replace )---
19
93
45
---( Take last digit )---
2
3
5
在以上例子中,map()
将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。下面代码示例:
// streams/FunctionMap2.java
// Different input and output types (不同的输入输出类型)
import java.util.*;
import java.util.stream.*;
class Numbered {
final int n;
Numbered(int n) {
this.n = n;
}
@Override
public String toString() {
return "Numbered(" + n + ")";
}
}
class FunctionMap2 {
public static void main(String[] args) {
Stream.of(1, 5, 7, 9, 11, 13)
.map(Numbered::new)
.forEach(System.out::println);
}
}
输出:
Numbered(1)
Numbered(5)
Numbered(7)
Numbered(9)
Numbered(11)
Numbered(13)
就是通过map函数可以进行流的转换。
如果使用 Function 返回的结果是数值类型的一种,我们必须使用合适的 mapTo数值类型
进行替代。代码示例:
// streams/FunctionMap3.java
// Producing numeric output streams( 产生数值输出流)
import java.util.*;
import java.util.stream.*;
class FunctionMap3 {
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));
}
}
输出:
5 7 9
17 19 23
17.000000 1.900000 0.230000
这个在开发里面还是用的比较多的
遗憾的是,Java 设计者并没有尽最大努力去消除基本类型。
在map()
中使用流
flatMap()
做了两件事:将产生流的函数应用在每个元素上(与 map()
所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。
flatMap(Function)
:当 Function
产生流时使用。
flatMapToInt(Function)
:当 Function
产生 IntStream
时使用。
flatMapToLong(Function)
:当 Function
产生 LongStream
时使用。
flatMapToDouble(Function)
:当 Function
产生 DoubleStream
时使用。
// streams/StreamOfStreams.java
import java.util.stream.*;
public class StreamOfStreams {
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
java.util.stream.ReferencePipeline$Head
java.util.stream.ReferencePipeline$Head
这个例子是设计好的,当给map
传入流的时候,我们期望得到元素,但实际上却得到了流对象。
// streams/FlatMap.java
import java.util.stream.*;
public class FlatMap {
public static void main(String[] args) {
Stream.of(1, 2, 3)
.flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
.forEach(System.out::println);
}
}
输出:
Gonzo
Fozzie
Beaker
Gonzo
Fozzie
Beaker
Gonzo
Fozzie
Beaker
使用flatMap
就可以解决这个问题。
// streams/StreamOfRandoms.java
import java.util.*;
import java.util.stream.*;
public class StreamOfRandoms {
static Random rand = new Random(47);
public static void main(String[] args) {
Stream.of(1, 2, 3, 4, 5)
.flatMapToInt(i -> IntStream.concat(
rand.ints(0, 100).limit(i), IntStream.of(-1)))
.forEach(n -> System.out.format("%d ", n));
}
}
输出:
58 -1 55 93 -1 61 61 29 -1 68 0 22 7 -1 88 28 51 89 9 -1
concat()
,它以参数顺序组合两个流,在每个随机 Integer
流的末尾添加一个 -1 作为标记。你可以看到最终流确实是从一组扁平流中创建的。
让我们再看一下将文件划分为单词流的任务。我们最后使用到的是 FileToWordsRegexp.java,它的问题是需要将整个文件读入行列表中 —— 显然需要存储该列表。而我们真正想要的是创建一个不需要中间存储层的单词流。
下面,我们再使用 flatMap()
来解决这个问题:
// streams/FileToWords.java
import java.nio.file.*;
import java.util.stream.*;
import java.util.regex.Pattern;
public class FileToWords {
public static Stream<String> stream(String filePath) throws Exception {
return Files.lines(Paths.get(filePath))
.skip(1) // First (comment) line
.flatMap(line ->
Pattern.compile("\\W+").splitAsStream(line));
}
}
stream()
现在是一个静态方法,因为它可以自己完成整个流创建过程。
Pattern.compile().splitAsStream()
必须把元素都收集到一个元素里面。而上面的方法则是创建了一个流。然后看代码:
// streams/FileToWordsTest.java
import java.util.stream.*;
public class FileToWordsTest {
public static void main(String[] args) throws Exception {
FileToWords.stream("Cheese.dat")
.limit(7)
.forEach(s -> System.out.format("%s ", s));
System.out.println();
FileToWords.stream("Cheese.dat")
.skip(7)
.limit(2)
.forEach(s -> System.out.format("%s ", s));
}
}
输出:
Not much of a cheese shop really
is it
因为有了真正的流(而不是FileToWordsRegexp.java
中基于集合存储的流),所以每次需要一个新的流时,我们都必须从头开始创建,因为流不能被复用。
Optional类
就是用来判空的,然后在流里面可以用来判断流是否为空。
Optional 可以实现这样的功能。一些标准流操作返回 Optional 对象,因为它们并不能保证预期结果一定存在。包括:
-
findFirst()
返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty -
findAny()
返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty -
max()
和min()
返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.emptyreduce()
不再以identity
形式开头,而是将其返回值包装在 Optional 中。(identity
对象成为其他形式的reduce()
的默认结果,因此不存在空结果的风险)
对于数字流 IntStream、LongStream 和 DoubleStream,average()
会将结果包装在 Optional 以防止流为空。
// streams/OptionalsFromEmptyStreams.java
import java.util.*;
import java.util.stream.*;
class OptionalsFromEmptyStreams {
public static void main(String[] args) {
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.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
OptionalDouble.empty
就是用到这些函数的时候,需要先解包,因为可能为空。
注意,空流是通过 Stream.<String>empty()
创建的。如果你在没有任何上下文环境的情况下调用 Stream.empty()
,Java 并不知道它的数据类型;这个语法解决了这个问题。
Optional基本用法:
// streams/OptionalBasics.java
import java.util.*;
import java.util.stream.*;
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());
}
}
输出:
Epithets
Nothing inside!
便利函数:
ifPresent(Consumer)
:当值存在时调用 Consumer,否则什么也不做。orElse(otherObject)
:如果值存在则直接返回,否则生成 otherObject。orElseGet(Supplier)
:如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。orElseThrow(Supplier)
:如果值存在直接返回,否则使用 Supplier 函数生成一个异常。
// streams/Optionals.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Optionals {
static void basics(Optional<String> optString) {
if(optString.isPresent())
System.out.println(optString.get());
else
System.out.println("Nothing inside!");
}
static void ifPresent(Optional<String> optString) {
optString.ifPresent(System.out::println);
}
static void orElse(Optional<String> optString) {
System.out.println(optString.orElse("Nada"));
}
static void orElseGet(Optional<String> optString) {
System.out.println(
optString.orElseGet(() -> "Generated"));
}
static void orElseThrow(Optional<String> optString) {
try {
System.out.println(optString.orElseThrow(
() -> new Exception("Supplied")));
} catch(Exception e) {
System.out.println("Caught " + e);
}
}
static void test(String testName, Consumer<Optional<String>> cos) {
System.out.println(" === " + testName + " === ");
cos.accept(Stream.of("Epithets").findFirst());
cos.accept(Stream.<String>empty().findFirst());
}
public static void main(String[] args) {
test("basics", Optionals::basics);
test("ifPresent", Optionals::ifPresent);
test("orElse", Optionals::orElse);
test("orElseGet", Optionals::orElseGet);
test("orElseThrow", Optionals::orElseThrow);
}
}
输出:
=== basics ===
Epithets
Nothing inside!
=== ifPresent ===
Epithets
=== orElse ===
Epithets
Nada
=== orElseGet ===
Epithets
Generated
=== orElseThrow ===
Epithets
Caught java.lang.Exception: Supplied
创建Optional
当我们在自己的代码中加入 Optional 时,可以使用下面 3 个静态方法:
empty()
:生成一个空 Optional。of(value)
:将一个非空值包装到 Optional 里。ofNullable(value)
:针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional 中。
// streams/CreatingOptionals.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
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"));
try {
test("of", Optional.of(null));
} catch(Exception e) {
System.out.println(e);
}
test("ofNullable", Optional.ofNullable("Hi"));
test("ofNullable", Optional.ofNullable(null));
}
}
输出:
=== empty ===
Null
=== of ===
Howdy
java.lang.NullPointerException
=== ofNullable ===
Hi
=== ofNullable ===
Null
我们不能通过传递 null
到 of()
来创建 Optional
对象。最安全的方法是, 使用 ofNullable()
来优雅地处理 null
。
Optional对象操作
当我们的流管道生成了 Optional 对象,下面 3 个方法可使得 Optional 的后续能做更多的操作:
filter(Predicate)
:对 Optional 中的内容应用Predicate 并将结果返回。如果 Optional 不满足 Predicate ,将 Optional 转化为空 Optional 。如果 Optional 已经为空,则直接返回空Optional 。map(Function)
:如果 Optional 不为空,应用 Function 于 Optional 中的内容,并返回结果。否则直接返回 Optional.empty。flatMap(Function)
:同map()
,但是提供的映射函数将结果包装在 Optional 对象中,因此flatMap()
不会在最后进行任何包装。
以上方法都不适用于数值型 Optional。一般来说,流的 filter()
会在 Predicate 返回 false
时移除流元素。而 Optional.filter()
在失败时不会删除 Optional,而是将其保留下来,并转化为空。下面请看代码示例:
// streams/OptionalFilter.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
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(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);
test("startsWith(\"B\")",
str -> str.startsWith("B"));
}
}
输出:
---( true )---
Optional[Foo]
Optional[]
Optional[Bar]
Optional[Baz]
Optional[Bingo]
Optional.empty
---( false )---
Optional.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
---( str != "" )---
Optional[Foo]
Optional.empty
Optional[Bar]
Optional[Baz]
Optional[Bingo]
Optional.empty
---( str.length() == 3 )---
Optional[Foo]
Optional.empty
Optional[Bar]
Optional[Baz]
Optional.empty
Optional.empty
---( startsWith("B") )---
Optional.empty
Optional.empty
Optional[Bar]
Optional[Baz]
Optional[Bingo]
Optional.empty
即使输出看起来像流,要特别注意 test()
中的 for 循环。每一次的for循环都重新启动流,然后跳过for循环索引指定的数量的元素,这就是流只剩后续元素的原因。然后调用findFirst()
获取剩余元素中的第一个元素,并包装在一个 Optional
对象中。
同 map()
一样 , Optional.map()
执行一个函数。它仅在 Optional 不为空时才执行这个映射函数。并将 Optional 的内容提取出来,传递给映射函数。代码示例:
// streams/OptionalMap.java
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Stream;
class OptionalMap {
static String[] elements = {"12", "", "23", "45"};
static Stream<String> testStream() {
return Arrays.stream(elements);
}
static void test(String descr, Function<String, String> func) {
System.out.println(" ---( " + descr + " )---");
for (int i = 0; i <= elements.length; i++) {
System.out.println(
testStream()
.skip(i)
.findFirst() // Produces an Optional
.map(func));
}
}
public static void main(String[] args) {
// If Optional is not empty, map() first extracts
// the contents which it then passes
// to the function:
test("Add brackets", s -> "[" + s + "]");
test("Increment", s -> {
try {
return Integer.parseInt(s) + 1 + "";
} catch (NumberFormatException e) {
return s;
}
});
test("Replace", s -> s.replace("2", "9"));
test("Take last digit", s -> s.length() > 0 ?
s.charAt(s.length() - 1) + "" : s);
}
// After the function is finished, map() wraps the
// result in an Optional before returning it:
}
输出:
---( Add brackets )---
Optional[[12]]
Optional[[]]
Optional[[23]]
Optional[[45]]
Optional.empty
---( Increment )---
Optional[13]
Optional[]
Optional[24]
Optional[46]
Optional.empty
---( Replace )---
Optional[19]
Optional[]
Optional[93]
Optional[45]
Optional.empty
---( Take last digit )---
Optional[2]
Optional[]
Optional[3]
Optional[5]
Optional.empty
Optional 的 flatMap()
应用于已生成 Optional 的映射函数,所以 flatMap()
不会像 map()
那样将结果封装在 Optional 中。代码示例:
// streams/OptionalFlatMap.java
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
class OptionalFlatMap {
static String[] elements = {"12", "", "23", "45"};
static Stream<String> testStream() {
return Arrays.stream(elements);
}
static void test(String descr,
Function<String, Optional<String>> func) {
System.out.println(" ---( " + descr + " )---");
for (int i = 0; i <= elements.length; i++) {
System.out.println(
testStream()
.skip(i)
.findFirst()
.flatMap(func));
}
}
public static void main(String[] args) {
test("Add brackets",
s -> Optional.of("[" + s + "]"));
test("Increment", s -> {
try {
return Optional.of(
Integer.parseInt(s) + 1 + "");
} catch (NumberFormatException e) {
return Optional.of(s);
}
});
test("Replace",
s -> Optional.of(s.replace("2", "9")));
test("Take last digit",
s -> Optional.of(s.length() > 0 ?
s.charAt(s.length() - 1) + ""
: s));
}
}
输出:
---( Add brackets )---
Optional[[12]]
Optional[[]]
Optional[[23]]
Optional[[45]]
Optional.empty
---( Increment )---
Optional[13]
Optional[]
Optional[24]
Optional[46]
Optional.empty
---( Replace )---
Optional[19]
Optional[]
Optional[93]
Optional[45]
Optional.empty
---( Take last digit )---
Optional[2]
Optional[]
Optional[3]
Optional[5]
Optional.empty
唯一的区别就是 flatMap()
不会把结果包装在 Optional 中,因为映射函数已经被包装过了。在如上示例中,我们已经在每一个映射函数中显式地完成了包装,但是很显然 Optional.flatMap()
是为那些自己已经生成 Optional 的函数而设计的。
Optional流
// streams/Signal.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
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 rand = new Random(47);
public static Signal morse() {
switch(rand.nextInt(4)) {
case 1: return new Signal("dot");
case 2: return new Signal("dash");
default: return null;
}
}
public static Stream<Optional<Signal>> stream() {
return Stream.generate(Signal::morse)
.map(signal -> Optional.ofNullable(signal));
}
}
当我们使用这个流的时候,必须要弄清楚如何解包 Optional。代码示例:
// streams/StreamOfOptionals.java
import java.util.*;
import java.util.stream.*;
public class StreamOfOptionals {
public static void main(String[] args) {
Signal.stream()
.limit(10)
.forEach(System.out::println);
System.out.println(" ---");
Signal.stream()
.limit(10)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(System.out::println);
}
}
输出:
Optional[Signal(dash)]
Optional[Signal(dot)]
Optional[Signal(dash)]
Optional.empty
Optional.empty
Optional[Signal(dash)]
Optional.empty
Optional[Signal(dot)]
Optional[Signal(dash)]
Optional[Signal(dash)]
---
Signal(dot)
Signal(dot)
Signal(dash)
Signal(dash)
在这里,我们使用 filter()
来保留那些非空 Optional,然后在 map()
中使用 get()
获取元素。由于每种情况都需要定义“空值”的含义,所以通常我们要为每个应用程序采用不同的方法。
终端操作
数组
toArray()
:将流转换成适当类型的数组。toArray(generator)
:在特殊情况下,生成自定义类型的数组。
当我们需要得到数组类型的数据以便于后续操作时,上面的方法就很有用。假设我们需要复用流产生的随机数时,就可以这么使用。代码示例:
// streams/RandInts.java
package streams;
import java.util.*;
import java.util.stream.*;
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()
:可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。
下例引入 parallel()
来帮助理解 forEachOrdered(Consumer)
的作用和使用场景。代码示例:
// streams/ForEach.java
import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class ForEach {
static final int SZ = 14;
public static void main(String[] args) {
rands().limit(SZ)
.forEach(n -> System.out.format("%d ", n));
System.out.println();
rands().limit(SZ)
.parallel()
.forEach(n -> System.out.format("%d ", n));
System.out.println();
rands().limit(SZ)
.parallel()
.forEachOrdered(n -> System.out.format("%d ", n));
}
}
输出:
258 555 693 861 961 429 868 200 522 207 288 128 551 589
551 861 429 589 200 522 555 693 258 128 868 288 961 207
258 555 693 861 961 429 868 200 522 207 288 128 551 589
在第一个流中,未使用 parallel()
,因此以元素从 rands()
出来的顺序输出结果。
在第二个流中,引入parallel()
,即便流很小,输出的结果的顺序也和前面不一样。这是由于多处理器并行操作的原因。
在最后一个流中,同时使用了 parallel()
和 forEachOrdered()
来强制保持原始流顺序。
集合
collect(Collector)
:使用 Collector 收集流元素到结果集合中。collect(Supplier, BiConsumer, BiConsumer)
:同上,第一个参数 Supplier 创建了一个新的结果集合,第二个参数 BiConsumer 将下一个元素收集到结果集合中,第三个参数 BiConsumer 用于将两个结果集合合并起来。
// streams/TreeSetOfWords.java
import java.util.*;
import java.nio.file.*;
import java.util.stream.*;
public class TreeSetOfWords {
public static void
main(String[] args) throws Exception {
Set<String> words2 =
Files.lines(Paths.get("TreeSetOfWords.java"))
.flatMap(s -> Arrays.stream(s.split("\\W+")))
.filter(s -> !s.matches("\\d+")) // No numbers
.map(String::trim)
.filter(s -> s.length() > 2)
.limit(100)
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(words2);
}
}
输出:
[Arrays, Collectors, Exception, Files, Output, Paths,
Set, String, System, TreeSet, TreeSetOfWords, args,
class, collect, file, filter, flatMap, get, import,
java, length, limit, lines, main, map, matches, new,
nio, numbers, out, println, public, split, static,
stream, streams, throws, toCollection, trim, util,
void, words2]
上面就是将数据存到TreeSet里面
Files.lines()
打开 Path 并将其转换成为由行组成的流。下一行代码以一个或多个非单词字符(\\W+
)为分界,对每一行进行分割,结果是产生一个数组,然后使用 Arrays.stream()
将数组转化成为流,最后flatMap()
将各行形成的多个单词流,扁平映射为一个单词流。使用 matches(\\d+)
查找并移除全部是数字的字符串(注意,words2
是通过的)。然后用 String.trim()
去除单词两边的空白,filter()
过滤所有长度小于3的单词,并只获取前100个单词,最后将其保存到 TreeSet 中。
// streams/MapCollector.java
import java.util.*;
import java.util.stream.*;
class Pair {
public final Character c;
public final Integer i;
Pair(Character c, Integer i) {
this.c = c;
this.i = i;
}
public Character getC() { return c; }
public Integer getI() { return i; }
@Override
public String toString() {
return "Pair(" + c + ", " + i + ")";
}
}
class RandomPair {
Random rand = new Random(47);
// An infinite iterator of random capital letters:
Iterator<Character> capChars = rand.ints(65,91)
.mapToObj(i -> (char)i)
.iterator();
public Stream<Pair> stream() {
return rand.ints(100, 1000).distinct()
.mapToObj(i -> new Pair(capChars.next(), i));
}
}
public class MapCollector {
public static void main(String[] args) {
Map<Integer, Character> map =
new RandomPair().stream()
.limit(8)
.collect(
Collectors.toMap(Pair::getI, Pair::getC));
System.out.println(map);
}
}
输出:
{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N}
Pair 只是一个基础的数据对象。RandomPair 创建了随机生成的 Pair 对象流。在 Java 中,我们不能直接以某种方式组合两个流。所以我创建了一个整数流,并且使用 mapToObj()
将整数流转化成为 Pair 流。 capChars的随机大写字母迭代器创建了流,然后next()
让我们可以在stream()
中使用这个流。就我所知,这是将多个流组合成新的对象流的唯一方法。
// streams/SpecialCollector.java
import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
public static void main(String[] args) throws Exception {
ArrayList<String> words =
FileToWords.stream("Cheese.dat")
.collect(ArrayList::new,
ArrayList::add,
ArrayList::addAll);
words.stream()
.filter(s -> s.equals("cheese"))
.forEach(System.out::println);
}
}
输出:
cheese
cheese
组合
reduce(BinaryOperator)
:使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional。reduce(identity, BinaryOperator)
:功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。reduce(identity, BiFunction, BinaryOperator)
:更复杂的使用形式(暂不介绍),这里把它包含在内,因为它可以提高效率。通常,我们可以显式地组合map()
和reduce()
来更简单的表达它。
// streams/Reduce.java
import java.util.*;
import java.util.stream.*;
class Frobnitz {
int size;
Frobnitz(int sz) { size = sz; }
@Override
public String toString() {
return "Frobnitz(" + size + ")";
}
// Generator:
static Random rand = new Random(47);
static final int BOUND = 100;
static Frobnitz supply() {
return new Frobnitz(rand.nextInt(BOUND));
}
}
public class Reduce {
public static void main(String[] args) {
Stream.generate(Frobnitz::supply)
.limit(10)
.peek(System.out::println)
.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
.ifPresent(System.out::println);
}
}
输出:
Frobnitz(58)
Frobnitz(55)
Frobnitz(93)
Frobnitz(61)
Frobnitz(61)
Frobnitz(29)
Frobnitz(68)
Frobnitz(0)
Frobnitz(22)
Frobnitz(7)
Frobnitz(29)
匹配
allMatch(Predicate)
:如果流的每个元素提供给 Predicate 都返回 true ,结果返回为 true。在第一个 false 时,则停止执行计算。anyMatch(Predicate)
:如果流的任意一个元素提供给 Predicate 返回 true ,结果返回为 true。在第一个 true 是停止执行计算。noneMatch(Predicate)
:如果流的每个元素提供给 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
// streams/Matching.java
// Demonstrates short-circuiting of *Match() operations
import java.util.stream.*;
import java.util.function.*;
import static streams.RandInts.*;
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);
}
}
输出:
1 2 3 4 5 6 7 8 9 true
1 2 3 4 false
1 true
1 2 3 4 5 6 7 8 9 false
1 false
1 2 3 4 5 6 7 8 9 true
查找
findFirst()
:返回第一个流元素的 Optional,如果流为空返回 Optional.empty。findAny(
:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
// streams/SelectElement.java
import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class SelectElement {
public static void main(String[] args) {
System.out.println(rands().findFirst().getAsInt());
System.out.println(
rands().parallel().findFirst().getAsInt());
System.out.println(rands().findAny().getAsInt());
System.out.println(
rands().parallel().findAny().getAsInt());
}
}
输出:
258
258
258
242
如果必须选择流中最后一个元素,那就使用 reduce()
。代码示例:
// streams/LastElement.java
import java.util.*;
import java.util.stream.*;
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));
// Non-numeric object:
Optional<String> lastobj =
Stream.of("one", "two", "three")
.reduce((n1, n2) -> n2);
System.out.println(
lastobj.orElse("Nothing there!"));
}
}
输出:
19
three
reduce()
的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果为数字流,你必须使用相近的数字 Optional 类型( numeric optional type),否则使用 Optional 类型,就像上例中的 Optional<String>
。
信息
count()
:流中的元素个数。max(Comparator)
:根据所传入的 Comparator 所决定的“最大”元素。min(Comparator)
:根据所传入的 Comparator 所决定的“最小”元素。
// streams/Informational.java
import java.util.stream.*;
import java.util.function.*;
public class Informational {
public static void
main(String[] args) throws Exception {
System.out.println(
FileToWords.stream("Cheese.dat").count());
System.out.println(
FileToWords.stream("Cheese.dat")
.min(String.CASE_INSENSITIVE_ORDER)
.orElse("NONE"));
System.out.println(
FileToWords.stream("Cheese.dat")
.max(String.CASE_INSENSITIVE_ORDER)
.orElse("NONE"));
}
}
输出:
32
a
you
数字流信息
average()
:求取流元素平均值。max()
和min()
:数值流操作无需 Comparator。sum()
:对所有流元素进行求和。summaryStatistics()
:生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。
// streams/NumericStreamInfo.java
import java.util.stream.*;
import static streams.RandInts.*;
public class NumericStreamInfo {
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());
}
}
输出:
507.94
998
8
50794
IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998}
小结
流式操作改变并极大地提升了 Java 语言的可编程性,并可能极大地阻止了 Java 编程人员向诸如 Scala 这种函数式语言的流转。在本书的剩余部分,我们将尽可能地使用流。