集合优化了对象的存储,而流和对象的处理有关。java
流是一系列与特定存储机制无关的元素——实际上,流并无“存储”之说。git
利用流,无需迭代集合中的元素,就能够提取和操做它们。这些管道一般被组合在一块儿,在流上造成一条操做管道。正则表达式
大多数状况下,将对象存储在集合是为了处理他们,所以你将会发现编程焦点从集合转移到了流。流的一个核心好处是,它使得程序更加短小而且更易理解。当 Lambda 表达式和方法引用和流一块儿使用的时候会让人感受自成一体。流使得 Java 8 更添魅力。编程
假如你要随机展现 5 至 20 之间不重复的整数并进行排序。实际上,你的关注点首先是建立一个有序集合。围绕这个集合进行后续操做。但使用流式编程,就能够简单陈述你想作什么:后端
// 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
复制代码
首先,咱们给 Random 对象一个种子(以便程序再次运行时产生相同的输出)。ints()
方法产生一个流而且 ints()
方法有多种方式的重载 — 两个参数限定了数值产生的边界。这将生成一个整数流。咱们可使用中间流操做(intermediate stream operation) distinct()
来获取它们的非重复值,而后使用 limit()
方法获取前 7 个元素。接下来,咱们使用 sorted()
方法排序。最终使用 forEach()
方法遍历输出,它根据传递给它的函数对每一个流对象执行操做。在这里,咱们传递了一个能够在控制台显示每一个元素的方法引用。System.out::println
。api
注意 Randoms.java
中没有声明任何变量。流能够在不使用赋值或可变数据的状况下对有状态的系统建模,这很是有用。数组
声明式编程(Declarative programming)是一种:声明要作什么,而非怎么作的编程风格。正如咱们在函数式编程中所看到的。注意,命令式编程的形式更难以理解。代码示例:安全
// 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);
}
}
复制代码
输出结果:markdown
[7, 8, 9, 11, 13, 15, 18]
复制代码
在 Randoms.java
中,咱们无需定义任何变量,但在这里咱们定义了 3 个变量: rand
,rints
和 r
。因为 nextInt()
方法没有下限的缘由(其内置的下限永远为 0),这段代码实现起来更复杂。因此咱们要生成额外的值来过滤小于 5 的结果。
注意,你必需要研究程序的真正意图,而在 Randoms.java
中,代码只是告诉了你它正在作什么。这种语义清晰性也是 Java 8 的流式编程更受推崇的重要缘由。
在 ImperativeRandoms.java
中显式地编写迭代机制称为外部迭代。而在 Randoms.java
中,流式编程采用内部迭代,这是流式编程的核心特性之一。这种机制使得编写的代码可读性更强,也更能利用多核处理器的优点。经过放弃对迭代过程的控制,咱们把控制权交给并行化机制。咱们将在并发编程一章中学习这部份内容。
另外一个重要方面,流是懒加载的。这表明着它只在绝对必要时才计算。你能够将流看做“延迟列表”。因为计算延迟,流使咱们可以表示很是大(甚至无限)的序列,而不须要考虑内存问题。
Java 设计者面临着这样一个难题:现存的大量类库不只为 Java 所用,同时也被应用在整个 Java 生态圈数百万行的代码中。如何将一个全新的流的概念融入到现有类库中呢?
好比在 Random 中添加更多的方法。只要不改变原有的方法,现有代码就不会受到干扰。
问题是,接口部分怎么改造呢?特别是涉及集合类接口的部分。若是你想把一个集合转换为流,直接向接口添加新方法会破坏全部老的接口实现类。
Java 8 采用的解决方案是:在接口中添加被 default
(默认
)修饰的方法。经过这种方案,设计者们能够将流式(stream)方法平滑地嵌入到现有类中。流方法预置的操做几乎已知足了咱们日常全部的需求。流操做的类型有三种:建立流,修改流元素(中间操做, Intermediate Operations),消费流元素(终端操做, Terminal Operations)。最后一种类型一般意味着收集流元素(一般是到集合中)。
下面咱们来看下每种类型的流操做。
你能够经过 Stream.of()
很容易地将一组元素转化成为流(Bubble
类在本章的后面定义):
// 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);
}
}
复制代码
输出结果:
Bubble(1)
Bubble(2)
Bubble(3)
It's a wonderful day for pie!
3.14159
2.718
1.618
复制代码
每一个集合均可经过 stream()
产生一个流。示例:
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);
}
}
复制代码
输出结果:
6
a pie! It's for wonderful day
phi: 1.618
e: 2.718
pi: 3.14159
复制代码
List<Bubble>
对象后,只需简单调用全部集合中都有的 stream()
。map()
会获取流中的全部元素,而且对流中元素应用操做从而产生新的元素,并将其传递到后续的流中。一般 map()
会获取对象并产生新的对象,但在这里产生了特殊的用于数值类型的流。例如,mapToInt()
方法将一个对象流(object stream)转换成为包含整型数字的 IntStream
。split()
来获取元素用于定义变量 w
。entrySet()
产生一个对象流,每一个对象都包含一个 key
键以及与其相关联的 value
值。而后分别调用 getKey()
和 getValue()
获取值。Random
类被一组生成流的方法加强了。代码示例:
// 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
++++++++
复制代码
为了消除冗余代码,我建立了一个泛型方法 show(Stream<T> stream)
(在讲解泛型以前就使用这个特性,确实有点做弊,可是回报是值得的)。类型参数 T
能够是任何类型,因此这个方法对 Integer、Long 和 Double 类型都生效。可是 Random 类只能生成基本类型 int, long, double 的流。幸运的是, boxed()
流操做将会自动地把基本类型包装成为对应的装箱类型,从而使得 show()
可以接受流。
咱们可使用 Random 为任意对象集合建立 Supplier。以下是一个文本文件提供字符串对象的例子。
Cheese.dat 文件内容:
// 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.
复制代码
咱们经过 File 类将 Cheese.dat 文件的全部行读取到 List<String>
中。代码示例:
// 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
复制代码
在这里你能够看到更为复杂的 split()
运用。在构造器中,每一行都被 split()
经过空格或者被方括号包裹的任意标点符号进行分割。在结束方括号后面的 +
表明 +
前面的东西能够出现一次或者屡次。
咱们注意到在构造函数中循环体使用命令式编程(外部迭代)。在之后的例子中,你甚至会看到咱们如何消除这一点。这种旧的形式虽不是特别糟糕,但使用流会让人感受更好。
在 toString()
和主方法中你看到了 collect()
收集操做,它根据参数来组合全部流中的元素。
当你使用 Collectors.joining()
,你将会获得一个 String
类型的结果,每一个元素都根据 joining()
的参数来进行分割。还有许多不一样的 Collectors
用于产生不一样的结果。
在主方法中,咱们提早看到了 Stream.generate()
的用法,它能够把任意 Supplier<T>
用于生成 T
类型的流。
IntStream
类提供了 range()
方法用于生成整型序列的流。编写循环时,这个方法会更加便利:
// 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());
}
}
复制代码
输出结果:
145
145
145
复制代码
在主方法中的第一种方式是咱们传统编写 for
循环的方式;第二种方式,咱们使用 range()
建立了流并将其转化为数组,而后在 for-in
代码块中使用。可是,若是你能像第三种方法那样全程使用流是更好的。咱们对范围中的数字进行求和。在流中能够很方便的使用 sum()
操做求和。
注意 IntStream.range()
相比 onjava.Range.range()
拥有更多的限制。这是因为其可选的第三个参数,后者容许步长大于 1,而且能够从大到小来生成。
实用小功能 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!
复制代码
原则上,在代码中包含并解释 repeat()
并不值得。诚然它是一个至关透明的工具,但结果取决于你的团队和公司的运做方式。
参照 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
复制代码
使用 Random.nextInt()
方法来挑选字母表中的大写字母。Random.nextInt()
的参数表明能够接受的最大的随机数范围,因此使用数组边界是通过深思熟虑的。
若是要建立包含相同对象的流,只须要传递一个生成那些对象的 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)
复制代码
这是建立单独工厂类(Separate Factory class)的另外一种方式。在不少方面它更加整洁,可是这是一个对于代码组织和品味的问题——你老是能够建立一个彻底不一样的工厂类。
Stream.iterate()
以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储做为第一个参数用于下次调用 iterate()
,依次类推。咱们能够利用 iterate()
生成一个斐波那契数列。代码示例:
// 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
复制代码
斐波那契数列将数列中最后两个元素进行求和以产生下一个元素。iterate()
只能记忆结果,所以咱们须要利用一个变量 x
追踪另一个元素。
在主方法中,咱们使用了一个以前没有见过的 skip()
操做。它根据参数丢弃指定数量的流元素。在这里,咱们丢弃了前 20 个元素。
在建造者设计模式(也称构造器模式)中,首先建立一个 builder
对象,传递给它多个构造器信息,最后执行“构造”。Stream 库提供了这样的 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
复制代码
注意,构造器会添加文件中的全部单词(除了第一行,它是包含文件路径信息的注释),可是其并无调用 build()
。只要你不调用 stream()
方法,就能够继续向 builder
对象中添加单词。
在该类的更完整形式中,你能够添加一个标志位用于查看 build()
是否被调用,而且可能的话增长一个能够添加更多单词的方法。在 Stream.Builder
调用 build()
方法后继续尝试添加单词会产生一个异常。
Arrays
类中含有一个名为 stream()
的静态方法用于把数组转换成为流。咱们能够重写 interfaces/Machine.java
中的主方法用于建立一个流,并将 execute()
应用于每个元素。代码示例:
// 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);
}
}
复制代码
输出结果:
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 的正则表达式将在字符串这一章节详细介绍。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);
}
}
复制代码
输出结果:
Not much of a cheese shop really is it
复制代码
在构造器中咱们读取了文件中的全部内容(跳过第一行注释,并将其转化成为单行字符串)。如今,当你调用 stream()
的时候,能够像往常同样获取一个流,但此次你能够屡次调用 stream()
在已存储的字符串中建立一个新的流。这里有个限制,整个文件必须存储在内存中;在大多数状况下这并非什么问题,可是这损失了流操做很是重要的优点:
幸运的是,咱们稍后就会知道如何解决这个问题。
中间操做用于从一个流中获取对象,并将对象做为另外一个流从后端输出,以链接到其余操做。
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
复制代码
FileToWords
稍后定义,但它的功能实现貌似和以前咱们看到的差很少:产生字符串对象的流。以后在其经过管道时调用 peek()
进行处理。
由于 peek()
符合无返回值的 Consumer 函数式接口,因此咱们只能观察,没法使用不一样的元素来替换流中的对象。
在 Randoms.java
中,咱们熟识了 sorted()
的默认比较器实现。其实它还有另外一种形式的实现:传入一个 Comparator 参数。代码示例:
// 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()
预设了一些默认的比较器。这里咱们使用的是反转“天然排序”。固然你也能够把 Lambda 函数做为参数传递给 sorted()
。
distinct()
:在 Randoms.java
类中的 distinct()
可用于消除流中的重复元素。相比建立一个 Set 集合,该方法的工做量要少得多。
filter(Predicate)
:过滤操做会保留与传递进去的过滤器函数计算结果为 true
的元素。
在下例中,isPrime()
做为过滤器函数,用于检测质数。
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
复制代码
在上面的自增示例中,咱们使用 Integer.parseInt()
尝试将一个字符串转化为整数。若是字符串不能转化成为整数就会抛出 NumberFormatException 异常,咱们只须回过头来将原始字符串放回到输出流中。
在以上例子中,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)
复制代码
咱们将获取到的整数经过构造器 Numbered::new
转化成为 Numbered
类型。
若是使用 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()
中组合流假设咱们如今有了一个传入的元素流,而且打算对流元素使用 map()
函数。如今你已经找到了一些可爱并独一无二的函数功能,可是问题来了:这个函数功能是产生一个流。咱们想要产生一个元素流,而实际却产生了一个元素流的流。
flatMap()
作了两件事:将产生流的函数应用在每一个元素上(与 map()
所作的相同),而后将每一个流都扁平化为元素,于是最终产生的仅仅是元素。
flatMap(Function)
:当 Function
产生流时使用。
flatMapToInt(Function)
:当 Function
产生 IntStream
时使用。
flatMapToLong(Function)
:当 Function
产生 LongStream
时使用。
flatMapToDouble(Function)
:当 Function
产生 DoubleStream
时使用。
为了弄清它的工做原理,咱们从传入一个刻意设计的函数给 map()
开始。该函数接受一个整数并产生一个字符串流:
// 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
复制代码
咱们天真地但愿可以获得字符串流,但实际获得的倒是“Head”流的流。咱们可使用 flatMap()
解决这个问题:
// 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
复制代码
从映射返回的每一个流都会自动扁平为组成它的字符串。
下面是另外一个演示,咱们从一个整数流开始,而后使用每个整数去建立更多的随机数。
// 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 做为标记。你能够看到最终流确实是从一组扁平流中建立的。
由于 rand.ints()
产生的是一个 IntStream
,因此我必须使用 flatMap()
、concat()
和 of()
的特定整数形式。
让咱们再看一下将文件划分为单词流的任务。咱们最后使用到的是 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()
如今是一个静态方法,由于它能够本身完成整个流建立过程。
注意:\\W+
是一个正则表达式。他表示“非单词字符”,+
表示“能够出现一次或者屡次”。小写形式的 \\w
表示“单词字符”。
咱们以前遇到的问题是 Pattern.compile().splitAsStream()
产生的结果为流,这意味着当咱们只是想要一个简单的单词流时,在传入的行流(stream of lines)上调用 map()
会产生一个单词流的流。幸运的是,flatMap()
能够将元素流的流扁平化为一个简单的元素流。或者,咱们可使用 String.split()
生成一个数组,其能够被 Arrays.stream()
转化成为流:
.flatMap(line -> Arrays.stream(line.split("\\W+"))))
复制代码
有了真正的、而非 FileToWordsRegexp.java
中基于集合存储的流,咱们每次使用都必须从头建立,由于流并不能被复用:
// 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
复制代码
在 System.out.format()
中的 %s
代表参数为 String 类型。
在咱们学习终端操做以前,咱们必须考虑若是你在一个空流中获取元素会发生什么。咱们喜欢为了“happy path”而将流链接起来,并假设流不会被中断。在流中放置 null
是很好的中断方法。那么是否有某种对象,可做为流元素的持有者,即便查看的元素不存在也能友好地提示咱们(也就是说,不会发生异常)?
Optional 能够实现这样的功能。一些标准流操做返回 Optional 对象,由于它们并不能保证预期结果必定存在。包括:
findFirst()
返回一个包含第一个元素的 Optional 对象,若是流为空则返回 Optional.emptyfindAny()
返回包含任意元素的 Optional 对象,若是流为空则返回 Optional.emptymax()
和 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
复制代码
当流为空的时候你会得到一个 Optional.empty 对象,而不是抛出异常。Optional 拥有 toString()
方法能够用于展现有用信息。
注意,空流是经过 Stream.<String>empty()
建立的。若是你在没有任何上下文环境的状况下调用 Stream.empty()
,Java 并不知道它的数据类型;这个语法解决了这个问题。若是编译器拥有了足够的上下文信息,好比:
Stream<String> s = Stream.empty();
复制代码
就能够在调用 empty()
时推断类型。
这个示例展现了 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!
复制代码
当你接收到 Optional 对象时,应首先调用 isPresent()
检查其中是否包含元素。若是存在,可以使用 get()
获取。
有许多便利函数能够解包 Optional ,这简化了上述“对所包含的对象的检查和执行操做”的过程:
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
复制代码
test()
经过传入全部方法都适用的 Consumer 来避免重复代码。
orElseThrow()
经过 catch 关键字来捕获抛出的异常。更多细节,将在 异常 这一章节中学习。
当咱们在本身的代码中加入 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 对象,下面 3 个方法可以使得 Optional 的后续能作更多的操做:
filter(Predicate)
:将 Predicate 应用于 Optional 中的内容并返回结果。当 Optional 不知足 Predicate 时返回空。若是 Optional 为空,则直接返回。
map(Function)
:若是 Optional 不为空,应用 Function 于 Optional 中的内容,并返回结果。不然直接返回 Optional.empty。
flatMap(Function)
:同 map()
,可是提供的映射函数将结果包装在 Optional 对象中,所以 flatMap()
不会在最后进行任何包装。
以上方法都不适用于数值型 Optional。 通常来讲,流的 filter()
会在 Predicate 返回 false
时移除流元素。 而 Optional.filter()
在失败时不会删除 Optional,而是将其保留下来,并转化为空。
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 中。
注意,不一样于普通 for 循环,这里的索引值范围并非 i < elements.length
, 而是 i <= elements.length
。因此最后一个元素实际上超出了流。方便的是,这将自动成为 Optional.empty,你能够在每个测试的结尾中看到。
同 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。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
复制代码
同 map()
,flatMap()
将提取非空 Optional 的内容并将其应用在映射函数。惟一的区别就是 flatMap()
不会把结果包装在 Optional 中,由于映射函数已经被包装过了。在如上示例中,咱们已经在每个映射函数中显式地完成了包装,可是很显然 Optional.flatMap()
是为那些本身已经生成 Optional 的函数而设计的。
假设你的生成器可能产生 null
值,那么当用它来建立流时,你会天然地想到用 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);
}
}
复制代码
上例将100个数值范围在 0 到 1000 之间的随机数流转换成为数组并将其存储在 rints
中。这样一来,每次调用 rands()
的时候能够重复获取相同的整数流。
forEach(Consumer)
常见如 System.out::println
做为 Consumer 函数。forEachOrdered(Consumer)
: 保证 forEach
按照原始流顺序操做。第一种形式:无序操做,仅在引入并行流时才有意义。在 并发编程 章节以前咱们不会深刻研究这个问题。这里简单介绍下 parallel()
:可实现多处理器并行操做。实现原理为将流分割为多个(一般数目为 CPU 核心数)并在不一样处理器上分别执行操做。由于咱们采用的是内部迭代,而不是外部迭代,因此这是可能实现的。
parallel()
看似简单,实则棘手。更多内容将在稍后的 并发编程 章节中学习。
下例引入 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
复制代码
为了方便测试不一样大小的数组,咱们抽离出了 SZ
变量。结果颇有趣:在第一个流中,未使用 parallel()
,因此 rands()
按照元素迭代出现的顺序显示结果;在第二个流中,引入parallel()
,即使流很小,输出的结果顺序也和前面不同。这是因为多处理器并行操做的缘由。屡次运行测试,结果均不一样。多处理器并行操做带来的非肯定性因素形成了这样的结果。
在最后一个流中,同时使用了 parallel()
和 forEachOrdered()
来强制保持原始流顺序。所以,对非并行流使用 forEachOrdered()
是没有任何影响的。
collect(Collector)
:使用 Collector 收集流元素到结果集合中。collect(Supplier, BiConsumer, BiConsumer)
:同上,第一个参数 Supplier 建立了一个新结果集合,第二个参数 BiConsumer 将下一个元素包含到结果中,第三个参数 BiConsumer 用于将两个值组合起来。在这里咱们只是简单介绍了几个 Collectors 的运用示例。实际上,它还有一些很是复杂的操做实现,可经过查看 java.util.stream.Collectors
的 API 文档了解。例如,咱们能够将元素收集到任意一种特定的集合中。
假设咱们如今为了保证元素有序,将元素存储在 TreeSet 中。Collectors 里面没有特定的 toTreeSet()
,可是咱们能够经过将集合的构造函数引用传递给 Collectors.toCollection()
,从而构建任何类型的集合。下面咱们来将一个文件中的单词收集到 TreeSet 集合中。代码示例:
// 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]
复制代码
Files.lines()
打开 Path 并将其转换成为行流。下一行代码将匹配一个或多个非单词字符(\\w+
)行进行分割,而后使用 Arrays.stream()
将其转化成为流,并将结果展平映射成为单词流。使用 matches(\\d+)
查找并移除全数字字符串(注意,words2
是经过的)。接下来咱们使用 String.trim()
去除单词两边的空白,filter()
过滤全部长度小于3的单词,紧接着只获取100个单词,最后将其保存到 TreeSet 中。
咱们也能够在流中生成 Map。代码示例:
// 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 随机生成的大写字母迭代器从流开始,而后 iterator()
容许咱们在 stream()
中使用它。就我所知,这是组合多个流以生成新的对象流的惟一方法。
在这里,咱们只使用最简单形式的 Collectors.toMap()
,这个方法值须要一个能够从流中获取键值对的函数。还有其余重载形式,其中一种形式是在遇到键值冲突时,须要一个函数来处理这种状况。
大多数状况下,java.util.stream.Collectors
中预设的 Collector 就能知足咱们的要求。除此以外,你还可使用第二种形式的 collect()
。 我把它留做更高级的练习,下例给出基本用法:
// 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
复制代码
在这里, ArrayList 的方法已经执行了你所须要的操做,可是彷佛更有可能的是,若是你必须使用这种形式的 collect()
,则必须本身建立特殊的定义。
reduce(BinaryOperator)
:使用 BinaryOperator 来组合全部流中的元素。由于流可能为空,其返回值为 Optional。reduce(identity, BinaryOperator)
:功能同上,可是使用 identity 做为其组合的初始值。所以若是流为空,identity 就是结果。reduce(identity, BiFunction, BinaryOperator)
:更复杂的使用形式(暂不介绍),这里把它包含在内,由于它能够提升效率。一般,咱们能够显式地组合 map()
和 reduce()
来更简单的表达它。下面来看下 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)
复制代码
Frobnitz 包含了一个名为 supply()
的生成器;由于这个方法对于 Supplier<Frobnitz>
是签名兼容的,咱们能够将其方法引用传递给 Stream.generate()
(这种签名兼容性被称做结构一致性)。无“初始值”的 reduce()
方法返回值是 Optional 类型。Optional.ifPresent()
只有在结果非空的时候才会调用 Consumer<Frobnitz>
(println
方法能够被调用是由于 Frobnitz 能够经过 toString()
方法转换成 String)。
Lambda 表达式中的第一个参数 fr0
是上一次调用 reduce()
的结果。而第二个参数 fr1
是从流传递过来的值。
reduce()
中的 Lambda 表达式使用了三元表达式来获取结果,当其长度小于 50 的时候获取 fr0
不然获取序列中的下一个值 fr1
。当取得第一个长度小于 50 的 Frobnitz
,只要获得结果就会忽略其余。这是个很是奇怪的约束, 也确实让咱们对 reduce()
有了更多的了解。
allMatch(Predicate)
:若是流的每一个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。在第一个 false 时,则中止执行计算。anyMatch(Predicate)
:若是流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。在第一个 false 是中止执行计算。noneMatch(Predicate)
:若是流的每一个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时中止执行计算。咱们已经在 Prime.java
中看到了 noneMatch()
的示例;allMatch()
和 anyMatch()
的用法基本上是等同的。下面咱们来探究一下短路行为。为了消除冗余代码,咱们建立了 show()
。首先咱们必须知道如何统一地描述这三个匹配器的操做,而后再将其转换为 Matcher 接口。代码示例:
// 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
复制代码
BiPredicate 是一个二元谓词,它只能接受两个参数且只返回 true 或者 false。它的第一个参数是咱们要测试的流,第二个参数是一个谓词 Predicate。Matcher 适用于全部的 Stream::*Match 方法,因此咱们能够传递每个到 show()
中。match.test()
的调用会被转换成 Stream::*Match 函数的调用。
show()
获取两个参数,Matcher 匹配器和用于表示谓词测试 n < val 中最大值的 val。这个方法生成一个1-9之间的整数流。peek()
是用于向咱们展现测试在短路以前的状况。从输出中能够看到每次都发生了短路。
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
复制代码
findFirst()
不管流是否为并行化的,老是会选择流中的第一个元素。对于非并行流,findAny()
会选择流中的第一个元素(即便从定义上来看是选择任意元素)。在这个例子中,咱们使用 parallel()
来并行流从而引入 findAny()
选择非第一个流元素的可能性。
若是必须选择流中最后一个元素,那就使用 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 所决定的“最小”元素。String 类型有预设的 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
复制代码
min()
和 max()
的返回类型为 Optional,这须要咱们使用 orElse()
来解包。
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}
复制代码
上例操做对于 LongStream 和 DoubleStream 一样适用。