流式编程
集合优化了对象的存储,流和对象的处理有关
利用流,我们无需迭代集合中的元素,就可以提取和操作它们。这些管道通常被组合在一起,在流上形成一条操作管道。
举个例子,假如你要随机展示 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
分析一下这段代码
1.首先给Random一个seed,保证每次运行产生相同的结果。
2.ints()会返回一个流对象,这样我们就对流进行进一步处理了。
public IntStream ints(int randomNumberOrigin, int randomNumberBound) { if (randomNumberOrigin >= randomNumberBound) throw new IllegalArgumentException(BadRange); return StreamSupport.intStream (new RandomIntsSpliterator (this, 0L, Long.MAX_VALUE, randomNumberOrigin, randomNumberBound), false); }
3.实际上后面的distinct(),sorted(),limit()都是对流对象处理返回一个流对象,这点是必要的,我们不能处理流对象后返回一个其他的对象。
4.foreach()这边是InputStream接口下的一个方法,利用了前面的方法引用
void forEach(IntConsumer action);
在这一段代码中没有声明一个变量,这里全程表达的是怎么做,如果是传统的声明式编程就像下面这段代码一样。
// 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); } }
实际上,它看起来反而更难理解了。
在 ImperativeRandoms.java
中显式地编写迭代机制称为外部迭代。而在 Randoms.java
中,流式编程采用内部迭代,这是流式编程的核心特性之一。这种机制使得编写的代码可读性更强,也更能利用多核处理器的优势。通过放弃对迭代过程的控制,我们把控制权交给并行化机制。我们将在并发编程一章中学习这部分内容。
另一个重要方面,流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。
流创建
// 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
// 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); } }
输出
6 a pie! It's for wonderful day phi: 1.618 e: 2.718 pi: 3.14159
mapToInt()
方法将一个对象流(object stream)转换成为包含整型数字的 IntStream
// 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()
能够接受流。
一段读文件内容的代码
// 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(" "))); } }
当你使用 Collectors.joining()
,你将会得到一个 String
类型的结果,每个元素都根据 joining()
的参数来进行分割。还有许多不同的 Collectors
用于产生不同的结果。
在主方法中,我们提前看到了 Stream.generate()
的用法,它可以把任意 Supplier<T>
用于生成 T
类型的流。
IntStream的一些小tip
// 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
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!
@FunctionalInterface public interface Runnable { public abstract void run(); }
这里我们的传给Runnable的是什么呢?虽然我觉得这边有点像多态的感觉,但是我们传的并不是一个类啊,是一个方法体,这边我暂时的理解是一个实现接口,以后明白了再补充上。
generate()
// 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); } }
输出
// 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); } }
// 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)
这个generate有点工厂类的味道,反正看起来很舒服,好理解,之前还对这类的感觉不好理解,现在想一下这样好像易读性更好。
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 个元素。
这里的函数表达式改变的就是外部变量x,因为并不是一个参数传过来的所以没有问题。
流的建造者模式
// 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); } }
这边需要对builder有了解,当然现在基本都用autoValue
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); } }
这里的基本就是一个steam将arrays转为流对象
// 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()
都有类似的操作。
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); }
现在,当你调用 stream()
的时候,可以像往常一样获取一个流,但这次你可以多次调用 stream()
在已存储的字符串中创建一个新的流。这里有个限制,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这损失了流操作非常重要的优势:
- 流“不需要存储”。当然它们需要一些内部存储,但是这只是序列的一小部分,和持有整个序列并不相同。
- 它们是懒加载计算的。
中间操作
// 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); } }
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); } }
移除元素
-
distinct()
:在Randoms.java
类中的distinct()
可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的工作量要少得多。 -
filter(Predicate)
:过滤操作会保留与传递进去的过滤器函数计算结果为true
元素。
这是一个检测质数例子
// 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()
操作一旦有失败就会退出。
这里用一个sqrt还挺精妙的,你只要在一个数的一半内不能被2整除,那么就是质数了。
应用函数到元素
-
map(Function)
:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。 -
mapToInt(ToIntFunction)
:操作同上,但结果是 IntStream。 -
mapToLong(ToLongFunction)
:操作同上,但结果是 LongStream。 -
mapToDouble(ToDoubleFunction)
:操作同上,但结果是 DoubleStream。
// 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); } }
输出
// 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); } }
在上面的自增示例中,我们使用 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
类型
返回数值类型的一种
// 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
在Map中组合流
假设我们现在有了一个传入的元素流,并且打算对流元素使用 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
// 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()
的特定整数形式。
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!
便利函数
// 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
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
。如果不加try catch也可以,但是运行会出错。
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)); } }
// 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)
终端操作
这里指的是不会给后端流返回什么,仅产生一个结果。
转换成数组
// 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()
操作时才有意义。在 并发编程 章节之前我们不会深入研究这个问题。这里简单介绍下 parallel()
:可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。
// 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,那么每次产生的应该都是一样的。但是加了parallel就会是不同的处理器处理的所以产生的也就不同。
收集
假设我们现在为了保证元素有序,将元素存储在 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); } }
第二个例子
// 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}
组合
// 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)
Lambda 表达式中的第一个参数 fr0
是上一次调用 reduce()
的结果。而第二个参数 fr1
是从流传递过来的值。
匹配
allMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。这个操作将会在第一个 false 之后短路;也就是不会在发生 false 之后继续执行计算。anyMatch(Predicate)
:如果流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。noneMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。这个操作将会在第一个 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
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()就不这样了。
如果要返回最后一个,就用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
信息
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")); } }
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}
算是对这块有了清晰的认知了,主要是明白了操作的思想。多加练习才能真正掌握。