第十四章 流式编程
流的一个核心好处是,它使得程序更加短小并且更易理解。当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体。流使得 Java 8 更具吸引力。
流式编程采用内部迭代。
流是懒加载的。
流支持
Java 8 采用的解决方案是:在接口中添加被 default(默认)修饰的方法。通过这种方案,设计者们可以将流式(stream)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
流创建
可以通过 Stream.of()
很容易地将一组元素转化成为流。
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);
Stream.of(3.14159, 2.718, 1.618).forEach(System.out::println);
每个集合都可以通过调用 stream()
方法来产生一个流。
Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
w.stream().map(x -> x + " ").forEach(System.out::print);
随机数流
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());
// Control the lower and upper bounds:
show(rand.ints(10, 20).boxed());
show(rand.longs(50, 100).boxed());
show(rand.doubles(20, 30).boxed());
// Control the stream size:
show(rand.ints(2).boxed());
show(rand.longs(2).boxed());
show(rand.doubles(2).boxed());
// Control the stream size and bounds:
show(rand.ints(3, 3, 9).boxed());
show(rand.longs(3, 12, 22).boxed());
show(rand.doubles(3, 11.5, 12.3).boxed());
}
}
int 类型的范围
IntStream.range():
System.out.println(IntStream.range(10, 20).sum());
public static void repeat(int n, Runnable action) {
IntStream.range(0, n).forEach(i -> action.run());
}
generate()
如果要创建包含相同对象的流,只需要传递一个生成那些对象 lambda 到generate()
中。
public class Generator implements Supplier<String> {
Random rand = new Random(47);
char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
@Override
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);
}
}
Stream.generate(() -> "duplicate").limit(3).forEach(System.out::println);
iterate()
Stream.iterate()
以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate()
,依次类推。
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) // Don't use the first 20
.limit(10) // Then take 10 of them
.forEach(System.out::println);
}
}
流的建造者模式
public class FileToWordsBuilder {
Stream.Builder<String> builder = Stream.builder();
public FileToWordsBuilder(String filePath) throws Exception {
Files.lines(Paths.get(filePath)).skip(1) // Skip the comment line at the beginning
.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);
}
}
Arrays
Arrays
类中含有一个名为 stream()
的静态方法用于把数组转换成为流。
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);
}
}
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();
// Select a subrange:
Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6).forEach(n -> System.out.format("%d ", n));
}
}
正则表达式
Java 8 在 java.util.regex.Pattern
中增加了一个新的方法 splitAsStream()
。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence
,因此不能将流作为 splitAsStream()
的参数。
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);
}
}
中间操作
中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。
跟踪和调试
peek()
操作的目的是帮助调试。它允许你无修改地查看流中的元素。
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);
}
}
流元素排序
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);
}
}
sorted()
预设了一些默认的比较器。这里我们使用的是反转“自然排序”。当然你也可以把 Lambda 函数作为参数传递给 sorted()
。
移除元素
-
distinct()
:在 Randoms.java 类中的 distinct() 可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的工作量要少得多。 -
filter(Predicate)
:过滤操作会保留与传递进去的过滤器函数计算结果为 true 元素。
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));
}
}
应用函数到元素
-
map(Function)
:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。 -
mapToInt(ToIntFunction)
:操作同上,但结果是IntStream
。 -
mapToLong(ToLongFunction)
:操作同上,但结果是LongStream
。 -
mapToDouble(ToDoubleFunction)
:操作同上,但结果是DoubleStream
。
使用 map()
映射多种函数到一个字符串流中:
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);
}
}
map()
将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。
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);
}
}
如果使用 Function
返回的结果是数值类型的一种,我们必须使用合适的 mapTo数值类型
进行替代。
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));
}
}
在 map() 中组合流
-
flatMap()
:获取流产生( stream-producing)函数,并将其应用于新到的元素(如 map() 所做的),然后获取每一个流并将其“扁平”为元素。所以它的输出只是元素。 -
flatMap(Function)
:当 Function 产生流时使用。 -
flatMapToInt(Function)
:当 Function 产生 IntStream 时使用。 -
flatMapToLong(Function)
:当 Function 产生 LongStream 时使用。 -
flatMapToDouble(Function)
:当 Function 产生 DoubleStream 时使用。
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));
}
}
Optional类
Optional
可以实现这样的功能。
可作为流元素的持有者,即使查看的元素不存在也能友好的提示我们(也就是说,没有异常)。
首先确保准流操作返回 Optional
对象,因为它们并不能保证预期结果一定存在。它们包括:
-
findFirst()
返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty -
findAny()
返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty -
max()
和min()
返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty -
reduce()
不再以 identity 形式开头,而是将其返回值包装在 Optional 中。(identity 对象成为其他形式的 reduce() 的默认结果,因此不存在空结果的风险) - 对于数字流 IntStream、LongStream 和 DoubleStream,
average()
会将结果包装在 Optional 以防止流为空。
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
对象时,应首先调用 isPresent()
检查其中是否包含元素。如果存在,可使用 get()
获取。
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());
}
}
便利函数
有许多便利函数可以解包 Optional
,这简化了上述“对所包含的对象的检查和执行操作”的过程:
-
ifPresent(Consumer)
:当值存在时调用 Consumer,否则什么也不做。 -
orElse(otherObject)
:如果值存在则直接返回,否则生成 otherObject。 -
orElseGet(Supplier)
:如果值存在直接生成对象,否则使用 Supplier 函数生成一个可替代对象。 -
orElseThrow(Supplier)
:如果值存在直接生成对象,否则使用 Supplier 函数生成一个异常。
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);
}
}
创建 Optional
当我们在自己的代码中加入 Optional
时,可以使用下面 3 个静态方法:
-
empty()
:生成一个空 Optional。 -
of(value)
:将一个非空值包装到 Optional 里。 -
ofNullable(value)
:针对一个可能为空的值,为空时自动生成Optional.empty
,否则将值包装在 Optional 中。
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));
}
}
Optional 对象操作
当我们的流管道生成了 Optional 对象,下面 3 个方法可使得 Optional 的后续能做更多的操作:
-
filter(Predicate)
:将 Predicate 应用于 Optional 中的内容并返回结果。当 Optional 不满足 Predicate 时返回空。如果 Optional 为空,则直接返回。 -
map(Function)
:如果 Optional 不为空,应用 Function 于 Optional 中的内容,并返回结果。否则直接返回 Optional.empty。 -
flatMap(Function)
:同 map(),但是提供的映射函数将结果包装在 Optional 对象中,因此 flatMap() 不会在最后进行任何包装。
一般来说,流的 filter()
会在 Predicate 返回 false 时删除流元素。而 Optional.filter()
在失败时不会删除 Optional,而是将其保留下来,并转化为空。
同 map()
一样 , Optional.map()
应用于函数。它仅在 Optional 不为空时才应用映射函数,并将 Optional 的内容提取到映射函数。
Optional 的 flatMap()
应用于已生成 Optional 的映射函数,所以 flatMap()
不会像 map()
那样将结果封装在 Optional 中。
Optional 流
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));
}
}
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);
}
}
终端操作
这些操作获取一个流并产生一个最终结果;它们不会像后端流提供任何东西。因此,终端操作总是你在管道中做的最后一件事情。
转化成数组(Convert to an Array)
-
toArray()
:将流转换成适当类型的数组。 -
toArray(generator)
:在特殊情况下,生成器用于分配你自己的数组存储。
public class RandInts {
private static int[] rints = new Random(47).ints(0, 1000).limit(100).toArray();
public static IntStream rands() {
return Arrays.stream(rints);
}
}
对每个元素应用最终操作(Apply a Final Operation to Every Element)
-
forEach(Consumer)
:你已经看到很多次System.out::println
作为 Consumer 函数。 -
forEachOrdered(Consumer)
: 这个形式保证了forEach
的操作顺序是原始流顺序。
public class ForEach {
static final int SZ = 1000;
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));
rands().limit(SZ).parallel().forEachOrdered(n -> {
System.out.println(Thread.currentThread().getName() + " -=- " + n);
});
}
}
收集(Collecting)
-
collect(Collector)
:使用Collector
来累计流元素到结果集合中。 -
collect(Supplier, BiConsumer, BiConsumer)
:同上,但是 Supplier 创建了一个新的结果集合,第一个 BiConsumer 是将下一个元素包含在结果中的函数,而第二个 BiConsumer 是用于将两个值组合起来。
组合所有流元素(Combining All Stream Elements)
-
reduce(BinaryOperator)
:使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional。 -
reduce(identity, BinaryOperator)
:功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。 -
reduce(identity, BiFunction, BinaryOperator)
:这个形式更为复杂(所以我们不会介绍它),在这里被提到是因为它使用起来会更有效。通常,你可以显示的组合 map() 和 reduce() 来更简单的表达这一点。
匹配(Matching)
-
allMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。这个操作将会在第一个 false 之后短路;也就是不会在发生 false 之后继续执行计算。 -
anyMatch(Predicate)
:如果流中的一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。 -
noneMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。
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);
}
}
选择元素
-
findFirst()
:返回一个含有第一个流元素的 Optional,如果流为空返回 Optional.empty。 -
findAny()
:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
信息(Informational)
-
count()
:流中的元素个数。 -
max(Comparator)
:根据所传入的 Comparator 所决定的“最大”元素。 -
min(Comparator)
:根据所传入的 Comparator 所决定的“最小”元素。
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"));
}
}
数字流信息(Information for Numeric Streams)
-
average()
:求取流元素平均值。 -
max()
和min()
:因为这些操作在数字流上面,所以不需要 Comparator。 -
sum()
:对所有流元素进行求和。 -
summaryStatistics()
:生成可能有用的数据。目前还不太清楚他们为什么觉得有必要这样做,但是你可以直接使用方法产生所有的数据。
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());
}
}