├── Figures ├── 2-Lambda.png ├── Stream.map.png ├── Stream.filter.png ├── Stream.distinct.png ├── Stream.flatMap.png ├── 2-AnonymousClass.png ├── Stream_pipeline_Sink.png ├── perf_Stream_min_int.png ├── Java_stream_Interfaces.png ├── Stream.reduce_parameter.png ├── Stream_pipeline_example.png ├── Stream_pipeline_naive.png ├── perf_Stream_min_String.png ├── perf_Stream_min_int_par.png ├── perf_Stream_reduction.png ├── JCF_Collection_Interfaces.png ├── Stream.collect_parameter.png ├── perf_Stream_reduction_par.png ├── Java_stream_pipeline_classes.png ├── WRONG_Java_stream_Interfaces.png └── perf_Stream_min_String_par.png ├── diaFiles ├── 2-Lambda.dia ├── Stream.map.dia ├── Stream.filter.dia ├── Stream.flatMap.dia ├── 2-AnonymousClass.dia ├── Stream.distinct.dia ├── Java_stream_Interfaces.dia ├── Stream_pipeline_Sink.dia ├── Stream_pipeline_naive.dia ├── Stream.collect_parameter.dia ├── Stream.reduce_parameter.dia ├── Stream_pipeline_example.dia ├── JCF_Collection_Interfaces.dia ├── Java_stream_pipeline_classes.dia └── WRONG_Java_stream_Interfaces.dia ├── perf ├── Stream_performance.xlsx └── StreamBenchmark │ ├── .classpath │ ├── .project │ └── src │ └── lee │ ├── TimeUtil.java │ ├── IntTest.java │ ├── StringTest.java │ └── ReductionTest.java ├── LICENSE ├── README.md ├── 2-Lambda and Anonymous Classes(II).md ├── 8-Stream Performance.md ├── 1-Lambda and Anonymous Classes(I).md ├── 4-Streams API(I).md ├── 5-Streams API(II).md ├── 6-Stream Pipelines.md └── 3-Lambda and Collections.md /Figures/2-Lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/2-Lambda.png -------------------------------------------------------------------------------- /Figures/Stream.map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream.map.png -------------------------------------------------------------------------------- /diaFiles/2-Lambda.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/2-Lambda.dia -------------------------------------------------------------------------------- /Figures/Stream.filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream.filter.png -------------------------------------------------------------------------------- /diaFiles/Stream.map.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream.map.dia -------------------------------------------------------------------------------- /Figures/Stream.distinct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream.distinct.png -------------------------------------------------------------------------------- /Figures/Stream.flatMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream.flatMap.png -------------------------------------------------------------------------------- /diaFiles/Stream.filter.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream.filter.dia -------------------------------------------------------------------------------- /diaFiles/Stream.flatMap.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream.flatMap.dia -------------------------------------------------------------------------------- /Figures/2-AnonymousClass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/2-AnonymousClass.png -------------------------------------------------------------------------------- /diaFiles/2-AnonymousClass.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/2-AnonymousClass.dia -------------------------------------------------------------------------------- /diaFiles/Stream.distinct.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream.distinct.dia -------------------------------------------------------------------------------- /perf/Stream_performance.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/perf/Stream_performance.xlsx -------------------------------------------------------------------------------- /Figures/Stream_pipeline_Sink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream_pipeline_Sink.png -------------------------------------------------------------------------------- /Figures/perf_Stream_min_int.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/perf_Stream_min_int.png -------------------------------------------------------------------------------- /Figures/Java_stream_Interfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Java_stream_Interfaces.png -------------------------------------------------------------------------------- /Figures/Stream.reduce_parameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream.reduce_parameter.png -------------------------------------------------------------------------------- /Figures/Stream_pipeline_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream_pipeline_example.png -------------------------------------------------------------------------------- /Figures/Stream_pipeline_naive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream_pipeline_naive.png -------------------------------------------------------------------------------- /Figures/perf_Stream_min_String.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/perf_Stream_min_String.png -------------------------------------------------------------------------------- /Figures/perf_Stream_min_int_par.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/perf_Stream_min_int_par.png -------------------------------------------------------------------------------- /Figures/perf_Stream_reduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/perf_Stream_reduction.png -------------------------------------------------------------------------------- /diaFiles/Java_stream_Interfaces.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Java_stream_Interfaces.dia -------------------------------------------------------------------------------- /diaFiles/Stream_pipeline_Sink.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream_pipeline_Sink.dia -------------------------------------------------------------------------------- /diaFiles/Stream_pipeline_naive.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream_pipeline_naive.dia -------------------------------------------------------------------------------- /Figures/JCF_Collection_Interfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/JCF_Collection_Interfaces.png -------------------------------------------------------------------------------- /Figures/Stream.collect_parameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Stream.collect_parameter.png -------------------------------------------------------------------------------- /Figures/perf_Stream_reduction_par.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/perf_Stream_reduction_par.png -------------------------------------------------------------------------------- /diaFiles/Stream.collect_parameter.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream.collect_parameter.dia -------------------------------------------------------------------------------- /diaFiles/Stream.reduce_parameter.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream.reduce_parameter.dia -------------------------------------------------------------------------------- /diaFiles/Stream_pipeline_example.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Stream_pipeline_example.dia -------------------------------------------------------------------------------- /Figures/Java_stream_pipeline_classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/Java_stream_pipeline_classes.png -------------------------------------------------------------------------------- /Figures/WRONG_Java_stream_Interfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/WRONG_Java_stream_Interfaces.png -------------------------------------------------------------------------------- /Figures/perf_Stream_min_String_par.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/Figures/perf_Stream_min_String_par.png -------------------------------------------------------------------------------- /diaFiles/JCF_Collection_Interfaces.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/JCF_Collection_Interfaces.dia -------------------------------------------------------------------------------- /diaFiles/Java_stream_pipeline_classes.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/Java_stream_pipeline_classes.dia -------------------------------------------------------------------------------- /diaFiles/WRONG_Java_stream_Interfaces.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollischuang/JavaLambdaInternals/HEAD/diaFiles/WRONG_Java_stream_Interfaces.dia -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ------Java Lambda Expressions Internals------ 2 | Author: CarpenterLee 3 | Name: 李豪 4 | Mail: hooleeucas@163.com 5 | URL: https://github.com/CarpenterLee/JavaLambdaInternals 6 | 7 | 欢迎转载,转载请注明出处,谢谢~~ 8 | -------------------------------------------------------------------------------- /perf/StreamBenchmark/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /perf/StreamBenchmark/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | StreamBenchmark 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /perf/StreamBenchmark/src/lee/TimeUtil.java: -------------------------------------------------------------------------------- 1 | package lee; 2 | 3 | public class TimeUtil { 4 | public static void outTimeMs(long startTime, String msg){ 5 | long ms = System.currentTimeMillis()-startTime; 6 | System.out.println(msg + " " + ms + " ms"); 7 | 8 | } 9 | public static void outTimeUs(long startTime, String msg){ 10 | long us = (System.nanoTime()-startTime+500)/1000; 11 | System.out.println(msg + " " + us + " us"); 12 | } 13 | public static void outTimeUs(long startTime, String msg, int times){ 14 | long ns_all = System.nanoTime()-startTime; 15 | double us_avg = (ns_all+500.0)/1000/times; 16 | System.out.println( 17 | String.format("%s avg of %d = %.2f us", msg, times, us_avg)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Functional Programming Internals 2 | 3 | # Authors 4 | 5 | | Name | Weibo Id | Blog | Mail | 6 | |:-----------|:-------------|:-------------|:-----------| 7 | | 李豪 |[@计算所的小鼠标](http://weibo.com/icttinymouse) | [CarpenterLee](http://www.cnblogs.com/CarpenterLee/) | hooleeucas@163.com | 8 | 9 | # Introduction 10 | 11 | 本文受启发于[JavaOne 2016](https://www.oracle.com/javaone/index.html)关于*Lambda表达式*的相关主题演讲[*Lambdas and Functiona*l Programming](https://blogs.oracle.com/thejavatutorials/entry/learn_java_8_lambdas_and)和[*Refactoring to Java 8*](https://blogs.oracle.com/thejavatutorials/entry/javaone_2016_refactoring_your_code)。 12 | 13 | Java 8已经发行两年多,但很多人仍然在使用JDK7。对企业来说,技术上谨慎未必是坏事,但对个人学习而言,不去学习新技术就很可能被技术抛弃。Java 8一个重要的变更是引入**函数式编程**和**Lambda表达式**(`lambda expression`),这听起来似乎很牛,有种我虽然不知道Lambda表达式是什么,但我仍然觉得很厉害的感觉。 14 | 15 | **Java stream包**是跟*Lambda表达式*同时添加新功能。**Lambda表达式只有跟stream一起使用才能显示其真实的威力**。 16 | 17 | 本系列文章不打算去争论“*什么才算是一门真正的函数式语言*”这类问题。我们会将**重点放在如何使用Java Lambda表达式,如何使用stream,以及二者背后的原理**。 18 | 19 | # Contents 20 | 21 | 具体内容安排如下: 22 | 23 | 1. [Lambda and Anonymous Classes(I)](./1-Lambda%20and%20Anonymous%20Classes(I).md),展示如何使用Lambda表达式替代匿名内部类,说明Lambda表达式和函数接口的关系。 24 | 2. [Lambda and Anonymous Classes(II)](./2-Lambda%20and%20Anonymous%20Classes(II).md),Lambda表达式的实现原理 25 | 3. [Lambda and Collections](./3-Lambda%20and%20Collections.md),学习Java集合框架(*Java Collections Framework*)新加入的方法 26 | 4. [Streams API(I)](./4-Streams%20API(I).md),Stream API基本用法 27 | 5. [Streams API(II)](./5-Streams%20API(II).md),Stream规约操作用法,顺道说明接口静态方法和默认方法以及方法引用的概念。 28 | 6. [Stream Pipelines](./6-Stream%20Pipelines.md),Stream流水线的实现原理 29 | 7. Stream并行实现原理(待写,>>欢迎感兴趣的同学完善<<) 30 | 8. [Stream Performance](./8-Stream%20Performance.md),Stream API性能评测 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /perf/StreamBenchmark/src/lee/IntTest.java: -------------------------------------------------------------------------------- 1 | package lee; 2 | 3 | import java.util.Arrays; 4 | import java.util.Random; 5 | /** 6 | * java -server -Xms10G -Xmx10G -XX:+PrintGCDetails 7 | * -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/IntTest 8 | * taskset -c 0-[0,1,3,7] java ... 9 | * @author CarpenterLee 10 | */ 11 | public class IntTest { 12 | 13 | public static void main(String[] args) { 14 | new IntTest().doTest(); 15 | } 16 | public void doTest(){ 17 | warmUp(); 18 | int[] lengths = { 19 | 10000, 20 | 100000, 21 | 1000000, 22 | 10000000, 23 | 100000000, 24 | 1000000000 25 | }; 26 | for(int length : lengths){ 27 | System.out.println(String.format("---array length: %d---", length)); 28 | int[] arr = new int[length]; 29 | randomInt(arr); 30 | 31 | int times = 4; 32 | int min1 = 1; 33 | int min2 = 2; 34 | int min3 = 3; 35 | long startTime; 36 | 37 | startTime = System.nanoTime(); 38 | for(int i=0; i list = randomStringList(length); 30 | int times = 4; 31 | String min1 = "1"; 32 | String min2 = "2"; 33 | String min3 = "3"; 34 | long startTime; 35 | 36 | startTime = System.nanoTime(); 37 | for(int i=0; i list = randomStringList(10); 60 | for(int i=0; i<20000; i++){ 61 | minStringForLoop(list); 62 | minStringStream(list); 63 | minStringParallelStream(list); 64 | 65 | } 66 | } 67 | private String minStringForLoop(ArrayList list){ 68 | String minStr = null; 69 | boolean first = true; 70 | for(String str : list){ 71 | if(first){ 72 | first = false; 73 | minStr = str; 74 | } 75 | if(minStr.compareTo(str)>0){ 76 | minStr = str; 77 | } 78 | } 79 | return minStr; 80 | } 81 | private String minStringStream(ArrayList list){ 82 | return list.stream().min(String::compareTo).get(); 83 | } 84 | private String minStringParallelStream(ArrayList list){ 85 | return list.stream().parallel().min(String::compareTo).get(); 86 | } 87 | private ArrayList randomStringList(int listLength){ 88 | ArrayList list = new ArrayList<>(listLength); 89 | Random rand = new Random(); 90 | int strLength = 10; 91 | StringBuilder buf = new StringBuilder(strLength); 92 | for(int i=0; i":()V 44 | 11: invokespecial #5 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V 45 | 14: invokevirtual #6 // Method java/lang/Thread.start:()V 46 | 17: return 47 | } 48 | 49 | ``` 50 | ## Lambda表达式实现 51 | 52 | **Lambda表达式通过*invokedynamic*指令实现,书写Lambda表达式不会产生新的类**。如果有如下代码,编译之后只有一个class文件: 53 | 54 | ```java 55 | public class MainLambda { 56 | public static void main(String[] args) { 57 | new Thread( 58 | () -> System.out.println("Lambda Thread run()") 59 | ).start();; 60 | } 61 | } 62 | ``` 63 | 编译之后的结果: 64 | 65 | ![2-Lambda](./Figures/2-Lambda.png) 66 | 67 | 通过javap反编译命名,我们更能看出Lambda表达式内部表示的不同: 68 | 69 | ```java 70 | // javap -c -p MainLambda.class 71 | public class MainLambda { 72 | ... 73 | public static void main(java.lang.String[]); 74 | Code: 75 | 0: new #2 // class java/lang/Thread 76 | 3: dup 77 | 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; /*使用invokedynamic指令调用*/ 78 | 9: invokespecial #4 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V 79 | 12: invokevirtual #5 // Method java/lang/Thread.start:()V 80 | 15: return 81 | 82 | private static void lambda$main$0(); /*Lambda表达式被封装成主类的私有方法*/ 83 | Code: 84 | 0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 85 | 3: ldc #7 // String Lambda Thread run() 86 | 5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 87 | 8: return 88 | } 89 | 90 | ``` 91 | 92 | 反编译之后我们发现Lambda表达式被封装成了主类的一个私有方法,并通过*invokedynamic*指令进行调用。 93 | 94 | ## 推论,this引用的意义 95 | 96 | 既然Lambda表达式不是内部类的简写,那么Lambda内部的`this`引用也就跟内部类对象没什么关系了。在Lambda表达式中`this`的意义跟在表达式外部完全一样。因此下列代码将输出两遍`Hello Hoolee`,而不是两个引用地址。 97 | 98 | ```Java 99 | public class Hello { 100 | Runnable r1 = () -> { System.out.println(this); }; 101 | Runnable r2 = () -> { System.out.println(toString()); }; 102 | public static void main(String[] args) { 103 | new Hello().r1.run(); 104 | new Hello().r2.run(); 105 | } 106 | public String toString() { return "Hello Hoolee"; } 107 | } 108 | ``` 109 | 110 | ## 参考文献 111 | 112 | http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html 113 | 114 | 115 | -------------------------------------------------------------------------------- /8-Stream Performance.md: -------------------------------------------------------------------------------- 1 | # Stream Performance 2 | 3 | 已经对Stream API的用法鼓吹够多了,用起简洁直观,但性能到底怎么样呢?会不会有很高的性能损失?本节我们对Stream API的性能一探究竟。 4 | 5 | 为保证测试结果真实可信,我们将JVM运行在`-server`模式下,测试数据在GB量级,测试机器采用常见的商用服务器,配置如下: 6 | 7 |
OSCentOS 6.7 x86_64
CPUIntel Xeon X5675, 12M Cache 3.06 GHz, 6 Cores 12 Threads
内存96GB
JDKjava version 1.8.0_91, Java HotSpot(TM) 64-Bit Server VM
8 | 9 | 测试[所用代码在这里](./perf/StreamBenchmark/src/lee),测试[结果汇总](./perf/Stream_performance.xlsx). 10 | 11 | ## 测试方法和测试数据 12 | 13 | 性能测试并不是容易的事,Java性能测试更费劲,因为虚拟机对性能的影响很大,JVM对性能的影响有两方面: 14 | 15 | 1. GC的影响。GC的行为是Java中很不好控制的一块,为增加确定性,我们手动指定使用CMS收集器,并使用10GB固定大小的堆内存。具体到JVM参数就是`-XX:+UseConcMarkSweepGC -Xms10G -Xmx10G` 16 | 2. JIT(Just-In-Time)即时编译技术。即时编译技术会将热点代码在JVM运行的过程中编译成本地代码,测试时我们会先对程序预热,触发对测试函数的即时编译。相关的JVM参数是`-XX:CompileThreshold=10000`。 17 | 18 | Stream并行执行时用到`ForkJoinPool.commonPool()`得到的线程池,为控制并行度我们使用Linux的`taskset`命令指定JVM可用的核数。 19 | 20 | 测试数据由程序随机生成。为防止一次测试带来的抖动,测试4次求出平均时间作为运行时间。 21 | 22 | 23 | 24 | ## 实验一 基本类型迭代 25 | 26 | 测试内容:找出整型数组中的最小值。对比for循环外部迭代和Stream API内部迭代性能。 27 | 28 | 测试程序[IntTest](./perf/StreamBenchmark/src/lee/IntTest.java),测试结果如下图: 29 | 30 | perf_Stream_min_int 31 | 32 | 图中展示的是for循环外部迭代耗时为基准的时间比值。分析如下: 33 | 34 | 1. 对于基本类型Stream串行迭代的性能开销明显高于外部迭代开销(两倍); 35 | 2. Stream并行迭代的性能比串行迭代和外部迭代都好。 36 | 37 | 并行迭代性能跟可利用的核数有关,上图中的并行迭代使用了全部12个核,为考察使用核数对性能的影响,我们专门测试了不同核数下的Stream并行迭代效果: 38 | 39 | perf_Stream_min_int_par 40 | 41 | 分析,对于基本类型: 42 | 43 | 1. 使用Stream并行API在单核情况下性能很差,比Stream串行API的性能还差; 44 | 2. 随着使用核数的增加,Stream并行效果逐渐变好,比使用for循环外部迭代的性能还好。 45 | 46 | 以上两个测试说明,对于基本类型的简单迭代,Stream串行迭代性能更差,但多核情况下Stream迭代时性能较好。 47 | 48 | 49 | ## 实验二 对象迭代 50 | 51 | 再来看对象的迭代效果。 52 | 53 | 测试内容:找出字符串列表中最小的元素(自然顺序),对比for循环外部迭代和Stream API内部迭代性能。 54 | 55 | 测试程序[StringTest](./perf/StreamBenchmark/src/lee/StringTest.java),测试结果如下图: 56 | 57 | perf_Stream_min_String 58 | 59 | 结果分析如下: 60 | 61 | 1. 对于对象类型Stream串行迭代的性能开销仍然高于外部迭代开销(1.5倍),但差距没有基本类型那么大。 62 | 2. Stream并行迭代的性能比串行迭代和外部迭代都好。 63 | 64 | 再来单独考察Stream并行迭代效果: 65 | 66 | perf_Stream_min_String_par 67 | 68 | 分析,对于对象类型: 69 | 70 | 1. 使用Stream并行API在单核情况下性能比for循环外部迭代差; 71 | 2. 随着使用核数的增加,Stream并行效果逐渐变好,多核带来的效果明显。 72 | 73 | 以上两个测试说明,对于对象类型的简单迭代,Stream串行迭代性能更差,但多核情况下Stream迭代时性能较好。 74 | 75 | ## 实验三 复杂对象归约 76 | 77 | 从实验一、二的结果来看,Stream串行执行的效果都比外部迭代差(很多),是不是说明Stream真的不行了?先别下结论,我们再来考察一下更复杂的操作。 78 | 79 | 测试内容:给定订单列表,统计每个用户的总交易额。对比使用外部迭代手动实现和Stream API之间的性能。 80 | 81 | 我们将订单简化为``构成的元组,并用`Order`对象来表示。测试程序[ReductionTest](./perf/StreamBenchmark/src/lee/ReductionTest.java),测试结果如下图: 82 | 83 | perf_Stream_reduction 84 | 85 | 分析,对于复杂的归约操作: 86 | 87 | 1. Stream API的性能普遍好于外部手动迭代,并行Stream效果更佳; 88 | 89 | 再来考察并行度对并行效果的影响,测试结果如下: 90 | 91 | perf_Stream_reduction_par 92 | 93 | 分析,对于复杂的归约操作: 94 | 95 | 1. 使用Stream并行归约在单核情况下性能比串行归约以及手动归约都要差,简单说就是最差的; 96 | 2. 随着使用核数的增加,Stream并行效果逐渐变好,多核带来的效果明显。 97 | 98 | 以上两个实验说明,对于复杂的归约操作,Stream串行归约效果好于手动归约,在多核情况下,并行归约效果更佳。我们有理由相信,对于其他复杂的操作,Stream API也能表现出相似的性能表现。 99 | 100 | 101 | ## 结论 102 | 103 | 上述三个实验的结果可以总结如下: 104 | 105 | 1. 对于简单操作,比如最简单的遍历,Stream串行API性能明显差于显示迭代,但并行的Stream API能够发挥多核特性。 106 | 2. 对于复杂操作,Stream串行API性能可以和手动实现的效果匹敌,在并行执行时Stream API效果远超手动实现。 107 | 108 | 所以,如果出于性能考虑,1. 对于简单操作推荐使用外部迭代手动实现,2. 对于复杂操作,推荐使用Stream API, 3. 在多核情况下,推荐使用并行Stream API来发挥多核优势,4.单核情况下不建议使用并行Stream API。 109 | 110 | 如果出于代码简洁性考虑,使用Stream API能够写出更短的代码。即使是从性能方面说,尽可能的使用Stream API也另外一个优势,那就是只要Java Stream类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。 111 | -------------------------------------------------------------------------------- /perf/StreamBenchmark/src/lee/ReductionTest.java: -------------------------------------------------------------------------------- 1 | package lee; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Random; 8 | import java.util.UUID; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * java -server -Xms10G -Xmx10G -XX:+PrintGCDetails 13 | * -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/ReductionTest 14 | * taskset -c 0-[0,1,3,7] java ... 15 | * @author CarpenterLee 16 | */ 17 | public class ReductionTest { 18 | 19 | public static void main(String[] args) { 20 | new ReductionTest().doTest(); 21 | } 22 | public void doTest(){ 23 | warmUp(); 24 | int[] lengths = { 25 | 10000, 26 | 100000, 27 | 1000000, 28 | 10000000, 29 | 20000000, 30 | 40000000 31 | }; 32 | for(int length : lengths){ 33 | System.out.println(String.format("---orders length: %d---", length)); 34 | List orders = Order.genOrders(length); 35 | int times = 4; 36 | Map map1 = null; 37 | Map map2 = null; 38 | Map map3 = null; 39 | 40 | long startTime; 41 | 42 | startTime = System.nanoTime(); 43 | for(int i=0; i System.out.println("Thread run()")// 省略接口名和方法名 31 | ).start(); 32 | ``` 33 | 34 | 上述代码跟匿名内部类的作用是一样的,但比匿名内部类更进一步。这里连**接口名和函数名都一同省掉**了,写起来更加神清气爽。如果函数体有多行,可以用大括号括起来,就像这样: 35 | 36 | ```Java 37 | // JDK8 Lambda表达式代码块写法 38 | new Thread( 39 | () -> { 40 | System.out.print("Hello"); 41 | System.out.println(" Hoolee"); 42 | } 43 | ).start(); 44 | ``` 45 | 46 | 47 | ## 例子2:带参函数的简写 48 | 49 | 如果要给一个字符串列表通过自定义比较器,按照字符串长度进行排序,Java 7的书写形式如下: 50 | 51 | ```Java 52 | // JDK7 匿名内部类写法 53 | List list = Arrays.asList("I", "love", "you", "too"); 54 | Collections.sort(list, new Comparator(){// 接口名 55 | @Override 56 | public int compare(String s1, String s2){// 方法名 57 | if(s1 == null) 58 | return -1; 59 | if(s2 == null) 60 | return 1; 61 | return s1.length()-s2.length(); 62 | } 63 | }); 64 | ``` 65 | 上述代码通过内部类重载了`Comparator`接口的`compare()`方法,实现比较逻辑。采用Lambda表达式可简写如下: 66 | 67 | ```Java 68 | // JDK8 Lambda表达式写法 69 | List list = Arrays.asList("I", "love", "you", "too"); 70 | Collections.sort(list, (s1, s2) ->{// 省略参数表的类型 71 | if(s1 == null) 72 | return -1; 73 | if(s2 == null) 74 | return 1; 75 | return s1.length()-s2.length(); 76 | }); 77 | ``` 78 | 上述代码跟匿名内部类的作用是一样的。除了省略了接口名和方法名,代码中把参数表的类型也省略了。这得益于`javac`的**类型推断**机制,编译器能够根据上下文信息推断出参数的类型,当然也有推断失败的时候,这时就需要手动指明参数类型了。注意,Java是强类型语言,每个变量和对象都必需有明确的类型。 79 | 80 | # 简写的依据 81 | 82 | 也许你已经想到了,**能够使用Lambda的依据是必须有相应的函数接口**(函数接口,是指内部只有一个抽象方法的接口)。这一点跟Java是强类型语言吻合,也就是说你并不能在代码的任何地方任性的写Lambda表达式。实际上*Lambda的类型就是对应函数接口的类型*。**Lambda表达式另一个依据是类型推断机制**,在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。Lambda表达更多合法的书写形式如下: 83 | 84 | ```Java 85 | // Lambda表达式的书写形式 86 | Runnable run = () -> System.out.println("Hello World");// 1 87 | ActionListener listener = event -> System.out.println("button clicked");// 2 88 | Runnable multiLine = () -> {// 3 代码块 89 | System.out.print("Hello"); 90 | System.out.println(" Hoolee"); 91 | }; 92 | BinaryOperator add = (Long x, Long y) -> x + y;// 4 93 | BinaryOperator addImplicit = (x, y) -> x + y;// 5 类型推断 94 | ``` 95 | 96 | 上述代码中,1展示了无参函数的简写;2处展示了有参函数的简写,以及类型推断机制;3是代码块的写法;4和5再次展示了类型推断机制。 97 | 98 | # 自定义函数接口 99 | 100 | 自定义函数接口很容易,只需要编写一个只有一个抽象方法的接口即可。 101 | 102 | ```Java 103 | // 自定义函数接口 104 | @FunctionalInterface 105 | public interface ConsumerInterface{ 106 | void accept(T t); 107 | } 108 | ``` 109 | 110 | 上面代码中的@FunctionalInterface是可选的,但加上该标注编译器会帮你检查接口是否符合函数接口规范。就像加入@Override标注会检查是否重载了函数一样。 111 | 112 | 有了上述接口定义,就可以写出类似如下的代码: 113 | 114 | `ConsumerInterface consumer = str -> System.out.println(str);` 115 | 116 | 进一步的,还可以这样使用: 117 | 118 | ```Java 119 | class MyStream{ 120 | private List list; 121 | ... 122 | public void myForEach(ConsumerInterface consumer){// 1 123 | for(T t : list){ 124 | consumer.accept(t); 125 | } 126 | } 127 | } 128 | MyStream stream = new MyStream(); 129 | stream.myForEach(str -> System.out.println(str));// 使用自定义函数接口书写Lambda表达式 130 | ``` 131 | 132 | # 参考文献 133 | 1. [The Java® Language Specification](https://docs.oracle.com/javase/specs/jls/se8/html/index.html) 134 | 2. http://viralpatel.net/blogs/lambda-expressions-java-tutorial/ 135 | 3. [《Java 8函数式编程 [英]沃伯顿》](https://www.amazon.cn/Java-8%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B-%E8%8B%B1-%E6%B2%83%E4%BC%AF%E9%A1%BF/dp/B00VDSW7AE) 136 | -------------------------------------------------------------------------------- /4-Streams API(I).md: -------------------------------------------------------------------------------- 1 | # Streams API(I) 2 | 3 | 你可能没意识到Java对函数式编程的重视程度,看看Java 8加入函数式编程扩充多少功能就清楚了。Java 8之所以费这么大功夫引入函数式编程,原因有二: 4 | 5 | 1. **代码简洁**函数式编程写出的代码简洁且意图明确,使用*stream*接口让你从此告别*for*循环。 6 | 2. **多核友好**,Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下`parallel()`方法。 7 | 8 | 这一节我们学习*stream*,也就是Java函数式编程的主角。对于Java 7来说*stream*完全是个陌生东西,*stream*并不是某种数据结构,它只是数据源的一种视图。这里的数据源可以是一个数组,Java容器或I/O channel等。正因如此要得到一个*stream*通常不会手动创建,而是调用对应的工具方法,比如: 9 | 10 | - 调用`Collection.stream()`或者`Collection.parallelStream()`方法 11 | - 调用`Arrays.stream(T[] array)`方法 12 | 13 | 常见的*stream*接口继承关系如图: 14 | 15 | Java_stream_Interfaces 16 | 17 | 图中4种*stream*接口继承自`BaseStream`,其中`IntStream, LongStream, DoubleStream`对应三种基本类型(`int, long, double`,注意不是包装类型),`Stream`对应所有剩余类型的*stream*视图。为不同数据类型设置不同*stream*接口,可以1.提高性能,2.增加特定接口函数。 18 | 19 |
20 | 21 | WRONG_Java_stream_Interfaces 22 | 23 | 你可能会奇怪为什么不把`IntStream`等设计成`Stream`的子接口?毕竟这接口中的方法名大部分是一样的。答案是这些方法的名字虽然相同,但是返回类型不同,如果设计成父子接口关系,这些方法将不能共存,因为Java不允许只有返回类型不同的方法重载。 24 | 25 | 虽然大部分情况下*stream*是容器调用`Collection.stream()`方法得到的,但*stream*和*collections*有以下不同: 26 | 27 | - **无存储**。*stream*不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。 28 | - **为函数式编程而生**。对*stream*的任何修改都不会修改背后的数据源,比如对*stream*执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新*stream*。 29 | - **惰式执行**。*stream*上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。 30 | - **可消费性**。*stream*只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。 31 | 32 | 对*stream*的操作分为为两类,**中间操作(*intermediate operations*)和结束操作(*terminal operations*)**,二者特点是: 33 | 34 | 1. __中间操作总是会惰式执行__,调用中间操作只会生成一个标记了该操作的新*stream*,仅此而已。 35 | 2. __结束操作会触发实际计算__,计算发生时会把所有中间操作积攒的操作以*pipeline*的方式执行,这样可以减少迭代次数。计算完成之后*stream*就会失效。 36 | 37 | 如果你熟悉Apache Spark RDD,对*stream*的这个特点应该不陌生。 38 | 39 | 下表汇总了`Stream`接口的部分常见方法: 40 | 41 | |操作类型|接口方法| 42 | |--------|--------| 43 | |中间操作|concat() distinct() filter() flatMap() limit() map() peek()
skip() sorted() parallel() sequential() unordered()| 44 | |结束操作|allMatch() anyMatch() collect() count() findAny() findFirst()
forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()| 45 | 46 | 区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为*stream*的大都是中间操作,否则是结束操作。 47 | 48 | ## stream方法使用 49 | 50 | *stream*跟函数接口关系非常紧密,没有函数接口*stream*就无法工作。回顾一下:__函数接口是指内部只有一个抽象方法的接口__。通常函数接口出现的地方都可以使用Lambda表达式,所以不必记忆函数接口的名字。 51 | 52 | ### forEach() 53 | 54 | 我们对`forEach()`方法并不陌生,在`Collection`中我们已经见过。方法签名为`void forEach(Consumer action)`,作用是对容器中的每个元素执行`action`指定的动作,也就是对元素进行遍历。 55 | 56 | ```Java 57 | // 使用Stream.forEach()迭代 58 | Stream stream = Stream.of("I", "love", "you", "too"); 59 | stream.forEach(str -> System.out.println(str)); 60 | ``` 61 | 由于`forEach()`是结束方法,上述代码会立即执行,输出所有字符串。 62 | 63 | ### filter() 64 | Stream filter 65 | 66 | 函数原型为`Stream filter(Predicate predicate)`,作用是返回一个只包含满足`predicate`条件元素的`Stream`。 67 | 68 | ```Java 69 | // 保留长度等于3的字符串 70 | Stream stream= Stream.of("I", "love", "you", "too"); 71 | stream.filter(str -> str.length()==3) 72 | .forEach(str -> System.out.println(str)); 73 | ``` 74 | 75 | 上述代码将输出为长度等于3的字符串`you`和`too`。注意,由于`filter()`是个中间操作,如果只调用`filter()`不会有实际计算,因此也不会输出任何信息。 76 | 77 | ### distinct() 78 | 79 | Stream distinct 80 | 81 | 函数原型为`Stream distinct()`,作用是返回一个去除重复元素之后的`Stream`。 82 | 83 | ```Java 84 | Stream stream= Stream.of("I", "love", "you", "too", "too"); 85 | stream.distinct() 86 | .forEach(str -> System.out.println(str)); 87 | ``` 88 | 89 | 上述代码会输出去掉一个`too`之后的其余字符串。 90 | 91 |

92 | 93 | ### sorted() 94 | 95 | 排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为`Stream sorted()`和`Stream sorted(Comparator comparator)`。 96 | 97 | ```Java 98 | Stream stream= Stream.of("I", "love", "you", "too"); 99 | stream.sorted((str1, str2) -> str1.length()-str2.length()) 100 | .forEach(str -> System.out.println(str)); 101 | ``` 102 | 103 | 上述代码将输出按照长度升序排序后的字符串,结果完全在预料之中。 104 | 105 | ### map() 106 | 107 | Stream map 108 | 109 | 函数原型为` Stream map(Function mapper)`,作用是返回一个对当前所有元素执行执行`mapper`之后的结果组成的`Stream`。直观的说,就是对每个元素按照某种操作进行转换,转换前后`Stream`中元素的个数不会改变,但元素的类型取决于转换之后的类型。 110 | 111 | ```Java 112 | Stream stream = Stream.of("I", "love", "you", "too"); 113 | stream.map(str -> str.toUpperCase()) 114 | .forEach(str -> System.out.println(str)); 115 | ``` 116 | 上述代码将输出原字符串的大写形式。 117 | 118 | ### flatMap() 119 | 120 | Stream flatMap 121 | 122 | 函数原型为` Stream flatMap(Function> mapper)`,作用是对每个元素执行`mapper`指定的操作,并用所有`mapper`返回的`Stream`中的元素组成一个新的`Stream`作为最终返回结果。说起来太拗口,通俗的讲`flatMap()`的作用就相当于把原*stream*中的所有元素都"摊平"之后组成的`Stream`,转换前后元素的个数和类型都可能会改变。 123 | 124 | ```Java 125 | Stream> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5)); 126 | stream.flatMap(list -> list.stream()) 127 | .forEach(i -> System.out.println(i)); 128 | ``` 129 | 130 | 上述代码中,原来的`stream`中有两个元素,分别是两个`List`,执行`flatMap()`之后,将每个`List`都“摊平”成了一个个的数字,所以会新产生一个由5个数字组成的`Stream`。所以最终将输出1~5这5个数字。 131 | 132 | ## 结语 133 | 134 | 截止到目前我们感觉良好,已介绍`Stream`接口函数理解起来并不费劲儿。如果你就此以为函数式编程不过如此,恐怕是高兴地太早了。下一节对`Stream`规约操作的介绍将刷新你现在的认识。 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /5-Streams API(II).md: -------------------------------------------------------------------------------- 1 | # Streams API(II) 2 | 3 | 上一节介绍了部分*Stream*常见接口方法,理解起来并不困难,但*Stream*的用法不止于此,本节我们将仍然以*Stream*为例,介绍流的规约操作。 4 | 5 | 规约操作(*reduction operation*)又被称作折叠操作(*fold*),是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。*Stream*类库有两个通用的规约操作`reduce()`和`collect()`,也有一些为简化书写而设计的专用规约操作,比如`sum()`、`max()`、`min()`、`count()`等。 6 | 7 | 最大或最小值这类规约操作很好理解(至少方法语义上是这样),我们着重介绍`reduce()`和`collect()`,这是比较有魔法的地方。 8 | 9 | ## 多面手reduce() 10 | 11 | *reduce*操作可以实现从一组元素中生成一个值,`sum()`、`max()`、`min()`、`count()`等都是*reduce*操作,将他们单独设为函数只是因为常用。`reduce()`的方法定义有三种重写形式: 12 | 13 | - `Optional reduce(BinaryOperator accumulator)` 14 | - `T reduce(T identity, BinaryOperator accumulator)` 15 | - ` U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)` 16 | 17 | 虽然函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数*identity*),或者是指定并行执行时多个部分结果的合并方式(参数*combiner*)。`reduce()`最常用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是不是觉得设计者有病。其实不然,因为“大”和“小”或者“求和"有时会有不同的语义。 18 | 19 | 需求:*从一组单词中找出最长的单词*。这里“大”的含义就是“长”。 20 | ```Java 21 | // 找出最长的单词 22 | Stream stream = Stream.of("I", "love", "you", "too"); 23 | Optional longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2); 24 | //Optional longest = stream.max((s1, s2) -> s1.length()-s2.length()); 25 | System.out.println(longest.get()); 26 | ``` 27 | 上述代码会选出最长的单词*love*,其中*Optional*是(一个)值的容器,使用它可以避免*null*值的麻烦。当然可以使用`Stream.max(Comparator comparator)`方法来达到同等效果,但`reduce()`自有其存在的理由。 28 | 29 | Stream.reduce_parameter 30 | 31 | 需求:*求出一组单词的长度之和*。这是个“求和”操作,操作对象输入类型是*String*,而结果类型是*Integer*。 32 | ```Java 33 | // 求单词长度之和 34 | Stream stream = Stream.of("I", "love", "you", "too"); 35 | Integer lengthSum = stream.reduce(0, // 初始值 // (1) 36 | (sum, str) -> sum+str.length(), // 累加器 // (2) 37 | (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3) 38 | // int lengthSum = stream.mapToInt(str -> str.length()).sum(); 39 | System.out.println(lengthSum); 40 | ``` 41 | 上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操作,使用`reduce()`函数将这两步合二为一,更有助于提升性能。如果想要使用`map()`和`sum()`组合来达到上述目的,也是可以的。 42 | 43 | `reduce()`擅长的是生成一个值,如果想要从*Stream*生成一个集合或者*Map*等复杂的对象该怎么办呢?终极武器`collect()`横空出世! 44 | 45 | ## >>> 终极武器collect() <<< 46 | 47 | 不夸张的讲,如果你发现某个功能在*Stream*接口中没找到,十有八九可以通过`collect()`方法实现。`collect()`是*Stream*接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子: 48 | ```Java 49 | // 将Stream转换成容器或Map 50 | Stream stream = Stream.of("I", "love", "you", "too"); 51 | List list = stream.collect(Collectors.toList()); // (1) 52 | // Set set = stream.collect(Collectors.toSet()); // (2) 53 | // Map map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3) 54 | ``` 55 | 上述代码分别列举了如何将*Stream*转换成*List*、*Set*和*Map*。虽然代码语义很明确,可是我们仍然会有几个疑问: 56 | 57 | 1. `Function.identity()`是干什么的? 58 | 2. `String::length`是什么意思? 59 | 3. *Collectors*是个什么东西? 60 | 61 | ## 接口的静态方法和默认方法 62 | 63 | *Function*是一个接口,那么`Function.identity()`是什么意思呢?这要从两方面解释: 64 | 65 | 1. Java 8允许在接口中加入具体方法。接口中的具体方法有两种,*default*方法和*static*方法,`identity()`就是*Function*接口的一个静态方法。 66 | 2. `Function.identity()`返回一个输出跟输入一样的Lambda表达式对象,等价于形如`t -> t`形式的Lambda表达式。 67 | 68 | 上面的解释是不是让你疑问更多?不要问我为什么接口中可以有具体方法,也不要告诉我你觉得`t -> t`比`identity()`方法更直观。我会告诉你接口中的*default*方法是一个无奈之举,在Java 7及之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,因为所有实现了该接口的类都要重新实现。试想在*Collection*接口中加入一个`stream()`抽象方法会怎样?*default*方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了*default*方法,为何不再加入*static*方法来避免专门的工具类呢! 69 | 70 | ## 方法引用 71 | 72 | 诸如`String::length`的语法形式叫做方法引用(*method references*),这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类: 73 | 74 | | 方法引用类别 | 举例 | 75 | |--------|--------| 76 | | 引用静态方法 | `Integer::sum` | 77 | | 引用某个对象的方法 | `list::add` | 78 | | 引用某个类的方法 | `String::length` | 79 | | 引用构造方法 | `HashMap::new` | 80 | 81 | 我们会在后面的例子中使用方法引用。 82 | 83 | ## 收集器 84 | 85 | 相信前面繁琐的内容已彻底打消了你学习Java函数式编程的热情,不过很遗憾,下面的内容更繁琐。但这不能怪Stream类库,因为要实现的功能本身很复杂。 86 | 87 | Stream.collect_parameter 88 | 89 | 收集器(*Collector*)是为`Stream.collect()`方法量身打造的工具接口(类)。考虑一下将一个*Stream*转换成一个容器(或者*Map*)需要做哪些工作?我们至少需要两样东西: 90 | 91 | 1. 目标容器是什么?是*ArrayList*还是*HashSet*,或者是个*TreeMap*。 92 | 2. 新元素如何添加到容器中?是`List.add()`还是`Map.put()`。 93 | 94 | 如果并行的进行规约,还需要告诉*collect()* 3. 多个部分结果如何合并成一个。 95 | 96 | 结合以上分析,*collect()*方法定义为` R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)`,三个参数依次对应上述三条分析。不过每次调用*collect()*都要传入这三个参数太麻烦,收集器*Collector*就是对这三个参数的简单封装,所以*collect()*的另一定义为` R collect(Collector collector)`。*Collectors*工具类可通过静态方法生成各种常用的*Collector*。举例来说,如果要将*Stream*规约成*List*可以通过如下两种方式实现: 97 | ```Java 98 | // 将Stream规约成List 99 | Stream stream = Stream.of("I", "love", "you", "too"); 100 | List list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1 101 | //List list = stream.collect(Collectors.toList());// 方式2 102 | System.out.println(list); 103 | ``` 104 | 通常情况下我们不需要手动指定*collect()*的三个参数,而是调用`collect(Collector collector)`方法,并且参数中的*Collector*对象大都是直接通过*Collectors*工具类获得。实际上传入的**收集器的行为决定了`collect()`的行为**。 105 | 106 | ## 使用collect()生成Collection 107 | 108 | 前面已经提到通过`collect()`方法将*Stream*转换成容器的方法,这里再汇总一下。将*Stream*转换成*List*或*Set*是比较常见的操作,所以*Collectors*工具已经为我们提供了对应的收集器,通过如下代码即可完成: 109 | ```Java 110 | // 将Stream转换成List或Set 111 | Stream stream = Stream.of("I", "love", "you", "too"); 112 | List list = stream.collect(Collectors.toList()); // (1) 113 | Set set = stream.collect(Collectors.toSet()); // (2) 114 | ``` 115 | 上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过`Collectors.toCollection(Supplier collectionFactory)`方法完成。 116 | ```Java 117 | // 使用toCollection()指定规约容器的类型 118 | ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3) 119 | HashSet hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4) 120 | ``` 121 | 上述代码(3)处指定规约结果是*ArrayList*,而(4)处指定规约结果为*HashSet*。一切如你所愿。 122 | 123 | ## 使用collect()生成Map 124 | 125 | 前面已经说过*Stream*背后依赖于某种数据源,数据源可以是数组、容器等,但不能是*Map*。反过来从*Stream*生成*Map*是可以的,但我们要想清楚*Map*的*key*和*value*分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下`collect()`的结果会是*Map*: 126 | 127 | 1. 使用`Collectors.toMap()`生成的收集器,用户需要指定如何生成*Map*的*key*和*value*。 128 | 2. 使用`Collectors.partitioningBy()`生成的收集器,对元素进行二分区操作时用到。 129 | 3. 使用`Collectors.groupingBy()`生成的收集器,对元素做*group*操作时用到。 130 | 131 | 情况1:使用`toMap()`生成的收集器,这种情况是最直接的,前面例子中已提到,这是和`Collectors.toCollection()`并列的方法。如下代码展示将学生列表转换成由<学生,GPA>组成的*Map*。非常直观,无需多言。 132 | ```Java 133 | // 使用toMap()统计学生GPA 134 | Map studentToGPA = 135 | students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key 136 | student -> computeGPA(student)));// 如何生成value 137 | ``` 138 | 情况2:使用`partitioningBy()`生成的收集器,这种情况适用于将`Stream`中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。 139 | ```Java 140 | // Partition students into passing and failing 141 | Map> passingFailing = students.stream() 142 | .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD)); 143 | ``` 144 | 情况3:使用`groupingBy()`生成的收集器,这是比较灵活的一种情况。跟SQL中的*group by*语句类似,这里的*groupingBy()*也是按照某个属性对数据进行分组,属性相同的元素会被对应到*Map*的同一个*key*上。下列代码展示将员工按照部门进行分组: 145 | ```Java 146 | // Group employees by department 147 | Map> byDept = employees.stream() 148 | .collect(Collectors.groupingBy(Employee::getDepartment)); 149 | ``` 150 | 以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL中使用*group by*是为了协助其他查询,比如*1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数*。Java类库设计者也考虑到了这种情况,增强版的`groupingBy()`能够满足这种需求。增强版的`groupingBy()`允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做**上游收集器**,之后执行其他运算的收集器叫做**下游收集器**(*downstream Collector*)。 151 | ```Java 152 | // 使用下游收集器统计每个部门的人数 153 | Map totalByDept = employees.stream() 154 | .collect(Collectors.groupingBy(Employee::getDepartment, 155 | Collectors.counting()));// 下游收集器 156 | ``` 157 | 上面代码的逻辑是不是越看越像SQL?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果*我们想得到每个员工的名字(字符串),而不是一个个*Employee*对象*,可通过如下方式做到: 158 | ```Java 159 | // 按照部门对员工分布组,并只保留员工的名字 160 | Map> byDept = employees.stream() 161 | .collect(Collectors.groupingBy(Employee::getDepartment, 162 | Collectors.mapping(Employee::getName,// 下游收集器 163 | Collectors.toList())));// 更下游的收集器 164 | ``` 165 | 如果看到这里你还没有对Java函数式编程失去信心,恭喜你,你已经顺利成为Java函数式编程大师了。 166 | 167 | ## 使用collect()做字符串join 168 | 169 | 这个肯定是大家喜闻乐见的功能,字符串拼接时使用`Collectors.joining()`生成的收集器,从此告别*for*循环。`Collectors.joining()`方法有三种重写形式,分别对应三种不同的拼接方式。无需多言,代码过目难忘。 170 | ```Java 171 | // 使用Collectors.joining()拼接字符串 172 | Stream stream = Stream.of("I", "love", "you"); 173 | //String joined = stream.collect(Collectors.joining());// "Iloveyou" 174 | //String joined = stream.collect(Collectors.joining(","));// "I,love,you" 175 | String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}" 176 | ``` 177 | ## collect()还可以做更多 178 | 179 | 除了可以使用*Collectors*工具类已经封装好的收集器,我们还可以自定义收集器,或者直接调用`collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)`方法,**收集任何形式你想要的信息**。不过*Collectors*工具类应该能满足我们的绝大部分需求,手动实现之间请先看看文档。 180 | 181 | # 参考文献 182 | 183 | 1. https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#package.description 184 | 2. https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html 185 | 3. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html 186 | 4. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html 187 | 5. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /6-Stream Pipelines.md: -------------------------------------------------------------------------------- 1 | # Stream Pipelines 2 | 3 | 前面我们已经学会如何使用Stream API,用起来真的很爽,但简洁的方法下面似乎隐藏着无尽的秘密,如此强大的API是如何实现的呢?比如Pipeline是怎么执行的,每次方法调用都会导致一次迭代吗?自动并行又是怎么做到的,线程个数是多少?本节我们学习Stream流水线的原理,这是Stream实现的关键所在。 4 | 5 | 首先回顾一下容器执行Lambda表达式的方式,以`ArrayList.forEach()`方法为例,具体代码如下: 6 | 7 | ```Java 8 | // ArrayList.forEach() 9 | public void forEach(Consumer action) { 10 | ... 11 | for (int i=0; modCount == expectedModCount && i < size; i++) { 12 | action.accept(elementData[i]);// 回调方法 13 | } 14 | ... 15 | } 16 | ``` 17 | 18 | 我们看到`ArrayList.forEach()`方法的主要逻辑就是一个*for*循环,在该*for*循环里不断调用`action.accept()`回调方法完成对元素的遍历。这完全没有什么新奇之处,回调方法在Java GUI的监听器中广泛使用。Lambda表达式的作用就是相当于一个回调方法,这很好理解。 19 | 20 | Stream API中大量使用Lambda表达式作为回调方法,但这并不是关键。理解Stream我们更关心的是另外两个问题:流水线和自动并行。使用Stream或许很容易写入如下形式的代码: 21 | 22 | ```Java 23 | int longestStringLengthStartingWithA 24 | = strings.stream() 25 | .filter(s -> s.startsWith("A")) 26 | .mapToInt(String::length) 27 | .max(); 28 | ``` 29 | 30 | 上述代码求出以字母*A*开头的字符串的最大长度,一种直白的方式是为每一次函数调用都执一次迭代,这样做能够实现功能,但效率上肯定是无法接受的。类库的实现着使用流水线(*Pipeline*)的方式巧妙的避免了多次迭代,其基本思想是在一次迭代中尽可能多的执行用户指定的操作。为讲解方便我们汇总了Stream的所有操作。 31 | 32 |
Stream操作分类
中间操作(Intermediate operations)无状态(Stateless)unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek()
有状态(Stateful)distinct() sorted() sorted() limit() skip()
结束操作(Terminal operations)非短路操作forEach() forEachOrdered() toArray() reduce() collect() max() min() count()
短路操作(short-circuiting)anyMatch() allMatch() noneMatch() findFirst() findAny()
33 | 34 | Stream上的所有操作分为两类:中间操作和结束操作,中间操作只是一种标记,只有结束操作才会触发实际计算。中间操作又可以分为无状态的(*Stateless*)和有状态的(*Stateful*),无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果;结束操作又可以分为短路操作和非短路操作,短路操作是指不用处理全部元素就可以返回结果,比如*找到第一个满足条件的元素*。之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。 35 | 36 | ## 一种直白的实现方式 37 | 38 | Stream_pipeline_naive 39 | 40 | 仍然考虑上述求最长字符串的程序,一种直白的流水线实现方式是为每一次函数调用都执一次迭代,并将处理中间结果放到某种数据结构中(比如数组,容器等)。具体说来,就是调用`filter()`方法后立即执行,选出所有以*A*开头的字符串并放到一个列表list1中,之后让list1传递给`mapToInt()`方法并立即执行,生成的结果放到list2中,最后遍历list2找出最大的数字作为最终结果。程序的执行流程如如所示: 41 | 42 | 这样做实现起来非常简单直观,但有两个明显的弊端: 43 | 44 | 1. 迭代次数多。迭代次数跟函数调用的次数相等。 45 | 2. 频繁产生中间结果。每次函数调用都产生一次中间结果,存储开销无法接受。 46 | 47 | 这些弊端使得效率底下,根本无法接受。如果不使用Stream API我们都知道上述代码该如何在一次迭代中完成,大致是如下形式: 48 | 49 | ```Java 50 | int longest = 0; 51 | for(String str : strings){ 52 | if(str.startsWith("A")){// 1. filter(), 保留以A开头的字符串 53 | int len = str.length();// 2. mapToInt(), 转换成长度 54 | longest = Math.max(len, longest);// 3. max(), 保留最长的长度 55 | } 56 | } 57 | ``` 58 | 59 | 采用这种方式我们不但减少了迭代次数,也避免了存储中间结果,显然这就是流水线,因为我们把三个操作放在了一次迭代当中。只要我们事先知道用户意图,总是能够采用上述方式实现跟Stream API等价的功能,但问题是Stream类库的设计者并不知道用户的意图是什么。如何在无法假设用户行为的前提下实现流水线,是类库的设计者要考虑的问题。 60 | 61 | ## Stream流水线解决方案 62 | 63 | 我们大致能够想到,应该采用某种方式记录用户每一步的操作,当用户调用结束操作时将之前记录的操作叠加到一起在一次迭代中全部执行掉。沿着这个思路,有几个问题需要解决: 64 | 65 | 1. 用户的操作如何记录? 66 | 2. 操作如何叠加? 67 | 3. 叠加之后的操作如何执行? 68 | 4. 执行后的结果(如果有)在哪里? 69 | 70 | ### >> 操作如何记录 71 | 72 | Java_stream_pipeline_classes 73 | 74 | 注意这里使用的是“*操作(operation)*”一词,指的是“Stream中间操作”的操作,很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<*数据来源,操作,回调函数*>构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的*PipelineHelper*来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线。跟Stream相关类和接口的继承关系图示。 75 | 76 | 还有*IntPipeline, LongPipeline, DoublePipeline*没在图中画出,这三个类专门为三种基本类型(不是包装类型)而定制的,跟*ReferencePipeline*是并列关系。图中*Head*用于表示第一个Stage,即调用调用诸如*Collection.stream()*方法产生的Stage,很显然这个Stage里不包含任何操作;*StatelessOp*和*StatefulOp*分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作。 77 | 78 | Stream流水线组织结构示意图如下: 79 | 80 | Stream_pipeline_example 81 | 82 | 图中通过`Collection.stream()`方法得到*Head*也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。**这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作**。这就是Stream记录操作的方式。 83 | 84 | ### >> 操作如何叠加 85 | 86 | 以上只是解决了操作记录的问题,要想让流水线起到应有的作用我们需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单,只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。 87 | 88 | 这种协议由*Sink*接口完成,*Sink*接口包含的方法如下表所示: 89 | 90 |
方法名作用
void begin(long size)开始遍历元素之前调用该方法,通知Sink做好准备。
void end()所有元素遍历完成之后调用,通知Sink没有更多的元素了。
boolean cancellationRequested()是否可以结束操作,可以让短路操作尽早结束。
void accept(T t)遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。
91 | 92 | 有了上面的协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的`accept()`方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink的`begin()`和`end()`方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个乘放结果的容器,而accept()方法负责将元素添加到该容器,最后end()负责对容器进行排序。对于短路操作,`Sink.cancellationRequested()`也是必须实现的,比如Stream.findFirst()是短路操作,只要找到一个元素,cancellationRequested()就应该返回*true*,以便调用者尽快结束查找。Sink的四个接口方法常常相互协作,共同完成计算任务。**实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法**。 93 | 94 | 有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。一种可能的Sink.accept()方法流程是这样的: 95 | 96 | ```Java 97 | void accept(U u){ 98 | 1. 使用当前Sink包装的回调函数处理u 99 | 2. 将处理结果传递给流水线下游的Sink 100 | } 101 | ``` 102 | 103 | Sink接口的其他几个方法也是按照这种[处理->转发]的模型实现。下面我们结合具体例子看看Stream的中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink的。先看Stream.map()方法: 104 | 105 | ```Java 106 | // Stream.map(),调用该方法将产生一个新的Stream 107 | public final Stream map(Function mapper) { 108 | ... 109 | return new StatelessOp(this, StreamShape.REFERENCE, 110 | StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) { 111 | @Override /*opWripSink()方法返回由回调函数包装而成Sink*/ 112 | Sink opWrapSink(int flags, Sink downstream) { 113 | return new Sink.ChainedReference(downstream) { 114 | @Override 115 | public void accept(P_OUT u) { 116 | R r = mapper.apply(u);// 1. 使用当前Sink包装的回调函数mapper处理u 117 | downstream.accept(r);// 2. 将处理结果传递给流水线下游的Sink 118 | } 119 | }; 120 | } 121 | }; 122 | } 123 | ``` 124 | 125 | 上述代码看似复杂,其实逻辑很简单,就是将回调函数*mapper*包装到一个Sink当中。由于Stream.map()是一个无状态的中间操作,所以map()方法返回了一个StatelessOp内部类对象(一个新的Stream),调用这个新Stream的opWripSink()方法将得到一个包装了当前回调函数的Sink。 126 | 127 | 再来看一个复杂一点的例子。Stream.sorted()方法将对Stream中的元素进行排序,显然这是一个有状态的中间操作,因为读取所有元素之前是没法得到最终顺序的。抛开模板代码直接进入问题本质,sorted()方法是如何将操作封装成Sink的呢?sorted()一种可能封装的Sink代码如下: 128 | 129 | ```Java 130 | // Stream.sort()方法用到的Sink实现 131 | class RefSortingSink extends AbstractRefSortingSink { 132 | private ArrayList list;// 存放用于排序的元素 133 | RefSortingSink(Sink downstream, Comparator comparator) { 134 | super(downstream, comparator); 135 | } 136 | @Override 137 | public void begin(long size) { 138 | ... 139 | // 创建一个存放排序元素的列表 140 | list = (size >= 0) ? new ArrayList((int) size) : new ArrayList(); 141 | } 142 | @Override 143 | public void end() { 144 | list.sort(comparator);// 只有元素全部接收之后才能开始排序 145 | downstream.begin(list.size()); 146 | if (!cancellationWasRequested) {// 下游Sink不包含短路操作 147 | list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink 148 | } 149 | else {// 下游Sink包含短路操作 150 | for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。 151 | if (downstream.cancellationRequested()) break; 152 | downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink 153 | } 154 | } 155 | downstream.end(); 156 | list = null; 157 | } 158 | @Override 159 | public void accept(T t) { 160 | list.add(t);// 1. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中 161 | } 162 | } 163 | ``` 164 | 165 | 上述代码完美的展现了Sink的四个接口方法是如何协同工作的: 166 | 1. 首先begin()方法告诉Sink参与排序的元素个数,方便确定中间结果容器的的大小; 167 | 2. 之后通过accept()方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素; 168 | 3. 最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink; 169 | 4. 如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理。 170 | 171 | ### >> 叠加之后的操作如何执行 172 | 173 | Stream_pipeline_Sink 174 | 175 | Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。 176 | 177 | 结束操作之后不能再有别的操作,所以结束操作不会创建新的流水线阶段(Stage),直观的说就是流水线的链表不会在往后延伸了。结束操作会创建一个包装了自己操作的Sink,这也是流水线中最后一个Sink,这个Sink只需要处理数据而不需要将结果传递给下游的Sink(因为没有下游)。对于Sink的[处理->转发]模型,结束操作的Sink就是调用链的出口。 178 | 179 | 我们再来考察一下上游的Sink是如何找到下游Sink的。一种可选的方案是在*PipelineHelper*中设置一个Sink字段,在流水线中找到下游Stage并访问Sink字段即可。但Stream类库的设计者没有这么做,而是设置了一个`Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)`方法来得到Sink,该方法的作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstream的Sink对象。为什么要产生一个新对象而不是返回一个Sink字段?这是因为使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。试想只要从流水线的最后一个Stage开始,不断调用上一个Stage的opWrapSink()方法直到最开始(不包括stage0,因为stage0代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的Sink,用代码表示就是这样: 180 | 181 | ```Java 182 | // AbstractPipeline.wrapSink() 183 | // 从下游向上游不断包装Sink。如果最初传入的sink代表结束操作, 184 | // 函数返回时就可以得到一个代表了流水线上所有操作的Sink。 185 | final Sink wrapSink(Sink sink) { 186 | ... 187 | for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) { 188 | sink = p.opWrapSink(p.previousStage.combinedFlags, sink); 189 | } 190 | return (Sink) sink; 191 | } 192 | ``` 193 | 194 | 现在流水线上从开始到结束的所有的操作都被包装到了一个Sink里,执行这个Sink就相当于执行整个流水线,执行Sink的代码如下: 195 | 196 | ```Java 197 | // AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。 198 | final void copyInto(Sink wrappedSink, Spliterator spliterator) { 199 | ... 200 | if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) { 201 | wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历 202 | spliterator.forEachRemaining(wrappedSink);// 迭代 203 | wrappedSink.end();// 通知遍历结束 204 | } 205 | ... 206 | } 207 | ``` 208 | 209 | 上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来,然后调用spliterator.forEachRemaining()方法对数据进行迭代(Spliterator是容器的一种迭代器,[参阅](https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/3-Lambda%20and%20Collections.md#spliterator)),最后调用wrappedSink.end()方法通知Sink数据处理结束。逻辑如此清晰。 210 | 211 | ### >> 执行后的结果在哪里 212 | 213 | 最后一个问题是流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里?首先要说明的是不是所有的Stream结束操作都需要返回结果,有些操作只是为了使用其副作用(*Side-effects*),比如使用`Stream.forEach()`方法将结果打印出来就是常见的使用副作用的场景(事实上,除了打印之外其他场景都应避免使用副作用),对于真正需要返回结果的结束操作结果存在哪里呢? 214 | 215 | > 特别说明:副作用不应该被滥用,也许你会觉得在Stream.forEach()里进行元素收集是个不错的选择,就像下面代码中那样,但遗憾的是这样使用的正确性和效率都无法保证,因为Stream可能会并行执行。大多数使用副作用的地方都可以使用[归约操作](./5-Streams%20API(II).md)更安全和有效的完成。 216 | 217 | ```Java 218 | // 错误的收集方式 219 | ArrayList results = new ArrayList<>(); 220 | stream.filter(s -> pattern.matcher(s).matches()) 221 | .forEach(s -> results.add(s)); // Unnecessary use of side-effects! 222 | // 正确的收集方式 223 | Listresults = 224 | stream.filter(s -> pattern.matcher(s).matches()) 225 | .collect(Collectors.toList()); // No side-effects! 226 | ``` 227 | 228 | 回到流水线执行结果的问题上来,需要返回结果的流水线结果存在哪里呢?这要分不同的情况讨论,下表给出了各种有返回结果的Stream结束操作。 229 | 230 |
返回类型对应的结束操作
booleananyMatch() allMatch() noneMatch()
OptionalfindFirst() findAny()
归约结果reduce() collect()
数组toArray()
231 | 232 | 1. 对于表中返回boolean或者Optional的操作(Optional是存放 一个 值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。 233 | 2. 对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过[收集器](./5-Streams%20API(II).md#收集器)指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用[reduce()](./5-Streams%20API(II).md#多面手reduce)方法实现的。 234 | 3. 对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做*Node*的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。关于Node的具体结构,我们会在下一节探究Stream如何并行执行时给出详细说明。 235 | 236 | ## 结语 237 | 238 | 本文详细介绍了Stream流水线的组织方式和执行过程,学习本文将有助于理解原理并写出正确的Stream代码,同时打消你对Stream API效率方面的顾虑。如你所见,Stream API实现如此巧妙,即使我们使用外部迭代手动编写等价代码,也未必更加高效。 239 | 240 | 241 | 注:留下本文所用的JDK版本,以便有考究癖的人考证: 242 | 243 | ```shell 244 | $ java -version 245 | java version "1.8.0_101" 246 | Java(TM) SE Runtime Environment (build 1.8.0_101-b13) 247 | Java HotSpot(TM) Server VM (build 25.101-b13, mixed mode) 248 | ``` 249 | -------------------------------------------------------------------------------- /3-Lambda and Collections.md: -------------------------------------------------------------------------------- 1 | # Lambda and Collections 2 | 3 | [TOC] 4 | 5 | ## 前言 6 | 7 | 我们先从最熟悉的*Java集合框架(Java Collections Framework, JCF)*开始说起。 8 | 9 | 为引入Lambda表达式,Java8新增了`java.util.function`包,里面包含常用的**函数接口**,这是Lambda表达式的基础,Java集合框架也新增部分接口,以便与Lambda表达式对接。 10 | 11 | 首先回顾一下Java集合框架的接口继承结构: 12 | 13 | ![JCF_Collection_Interfaces](./Figures/JCF_Collection_Interfaces.png) 14 | 15 | 上图中绿色标注的接口类,表示在Java8中加入了新的接口方法,当然由于继承关系,他们相应的子类也都会继承这些新方法。下表详细列举了这些方法。 16 | 17 | | 接口名 | Java8新加入的方法 | 18 | |--------|--------| 19 | | Collection |removeIf() spliterator() stream() parallelStream() forEach()| 20 | | List |replaceAll() sort()| 21 | | Map |getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()| 22 | 23 | 这些新加入的方法大部分要用到`java.util.function`包下的接口,这意味着这些方法大部分都跟Lambda表达式相关。我们将逐一学习这些方法。 24 | 25 | ## Collection中的新方法 26 | 27 | 如上所示,接口`Collection`和`List`新加入了一些方法,我们以是`List`的子类`ArrayList`为例来说明。了解[Java7`ArrayList`实现原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/2-ArrayList.md),将有助于理解下文。 28 | 29 | ### forEach() 30 | 31 | 该方法的签名为`void forEach(Consumer action)`,作用是对容器中的每个元素执行`action`指定的动作,其中`Consumer`是个函数接口,里面只有一个待实现方法`void accept(T t)`(后面我们会看到,这个方法叫什么根本不重要,你甚至不需要记忆它的名字)。 32 | 33 | 需求:*假设有一个字符串列表,需要打印出其中所有长度大于3的字符串.* 34 | 35 | Java7及以前我们可以用增强的for循环实现: 36 | 37 | ```Java 38 | // 使用曾强for循环迭代 39 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 40 | for(String str : list){ 41 | if(str.length()>3) 42 | System.out.println(str); 43 | } 44 | ``` 45 | 46 | 现在使用`forEach()`方法结合匿名内部类,可以这样实现: 47 | 48 | ```Java 49 | // 使用forEach()结合匿名内部类迭代 50 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 51 | list.forEach(new Consumer(){ 52 | @Override 53 | public void accept(String str){ 54 | if(str.length()>3) 55 | System.out.println(str); 56 | } 57 | }); 58 | ``` 59 | 上述代码调用`forEach()`方法,并使用匿名内部类实现`Comsumer`接口。到目前为止我们没看到这种设计有什么好处,但是不要忘记Lambda表达式,使用Lambda表达式实现如下: 60 | ```Java 61 | // 使用forEach()结合Lambda表达式迭代 62 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 63 | list.forEach( str -> { 64 | if(str.length()>3) 65 | System.out.println(str); 66 | }); 67 | ``` 68 | 上述代码给`forEach()`方法传入一个Lambda表达式,我们不需要知道`accept()`方法,也不需要知道`Consumer`接口,类型推导帮我们做了一切。 69 | 70 | ### removeIf() 71 | 72 | 该方法签名为`boolean removeIf(Predicate filter)`,作用是**删除容器中所有满足`filter`指定条件的元素**,其中`Predicate`是一个函数接口,里面只有一个待实现方法`boolean test(T t)`,同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。 73 | 74 | 需求:*假设有一个字符串列表,需要删除其中所有长度大于3的字符串。* 75 | 76 | 我们知道如果需要在迭代过程冲对容器进行删除操作必须使用迭代器,否则会抛出`ConcurrentModificationException`,所以上述任务传统的写法是: 77 | 78 | ```Java 79 | // 使用迭代器删除列表元素 80 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 81 | Iterator it = list.iterator(); 82 | while(it.hasNext()){ 83 | if(it.next().length()>3) // 删除长度大于3的元素 84 | it.remove(); 85 | } 86 | ``` 87 | 88 | 现在使用`removeIf()`方法结合匿名内部类,我们可是这样实现: 89 | ```Java 90 | // 使用removeIf()结合匿名名内部类实现 91 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 92 | list.removeIf(new Predicate(){ // 删除长度大于3的元素 93 | @Override 94 | public boolean test(String str){ 95 | return str.length()>3; 96 | } 97 | }); 98 | ``` 99 | 上述代码使用`removeIf()`方法,并使用匿名内部类实现`Precicate`接口。相信你已经想到用Lambda表达式该怎么写了: 100 | 101 | ```Java 102 | // 使用removeIf()结合Lambda表达式实现 103 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 104 | list.removeIf(str -> str.length()>3); // 删除长度大于3的元素 105 | ``` 106 | 使用Lambda表达式不需要记忆`Predicate`接口名,也不需要记忆`test()`方法名,只需要知道此处需要一个返回布尔类型的Lambda表达式就行了。 107 | 108 | ### replaceAll() 109 | 110 | 该方法签名为`void replaceAll(UnaryOperator operator)`,作用是**对每个元素执行`operator`指定的操作,并用操作结果来替换原来的元素**。其中`UnaryOperator`是一个函数接口,里面只有一个待实现函数`T apply(T t)`。 111 | 112 | 需求:*假设有一个字符串列表,将其中所有长度大于3的元素转换成大写,其余元素不变。* 113 | 114 | Java7及之前似乎没有优雅的办法: 115 | 116 | ```Java 117 | // 使用下标实现元素替换 118 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 119 | for(int i=0; i3) 122 | list.set(i, str.toUpperCase()); 123 | } 124 | ``` 125 | 126 | 使用`replaceAll()`方法结合匿名内部类可以实现如下: 127 | ```Java 128 | // 使用匿名内部类实现 129 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 130 | list.replaceAll(new UnaryOperator(){ 131 | @Override 132 | public String apply(String str){ 133 | if(str.length()>3) 134 | return str.toUpperCase(); 135 | return str; 136 | } 137 | }); 138 | ``` 139 | 上述代码调用`replaceAll()`方法,并使用匿名内部类实现`UnaryOperator`接口。我们知道可以用更为简洁的Lambda表达式实现: 140 | ```Java 141 | // 使用Lambda表达式实现 142 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 143 | list.replaceAll(str -> { 144 | if(str.length()>3) 145 | return str.toUpperCase(); 146 | return str; 147 | }); 148 | ``` 149 | ### sort() 150 | 151 | 该方法定义在`List`接口中,方法签名为`void sort(Comparator c)`,该方法**根据`c`指定的比较规则对容器元素进行排序**。`Comparator`接口我们并不陌生,其中有一个方法`int compare(T o1, T o2)`需要实现,显然该接口是个函数接口。 152 | 153 | 需求:*假设有一个字符串列表,按照字符串长度增序对元素排序。* 154 | 155 | 由于Java7以及之前`sort()`方法在`Collections`工具类中,所以代码要这样写: 156 | 157 | ```Java 158 | // Collections.sort()方法 159 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 160 | Collections.sort(list, new Comparator(){ 161 | @Override 162 | public int compare(String str1, String str2){ 163 | return str1.length()-str2.length(); 164 | } 165 | }); 166 | ``` 167 | 168 | 现在可以直接使用`List.sort()方法`,结合Lambda表达式,可以这样写: 169 | 170 | ```Java 171 | // List.sort()方法结合Lambda表达式 172 | ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); 173 | list.sort((str1, str2) -> str1.length()-str2.length()); 174 | ``` 175 | 176 | ### spliterator() 177 | 178 | 方法签名为`Spliterator spliterator()`,该方法返回容器的**可拆分迭代器**。从名字来看该方法跟`iterator()`方法有点像,我们知道`Iterator`是用来迭代容器的,`Spliterator`也有类似作用,但二者有如下不同: 179 | 180 | 1. `Spliterator`既可以像`Iterator`那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。 181 | 2. `Spliterator`是可拆分的,一个`Spliterator`可以通过调用`Spliterator trySplit()`方法来尝试分成两个。一个是`this`,另一个是新返回的那个,这两个迭代器代表的元素没有重叠。 182 | 183 | 可通过(多次)调用`Spliterator.trySplit()`方法来分解负载,以便多线程处理。 184 | 185 | ### stream()和parallelStream() 186 | 187 | `stream()`和`parallelStream()`分别**返回该容器的`Stream`视图表示**,不同之处在于`parallelStream()`返回并行的`Stream`。**`Stream`是Java函数式编程的核心类**,我们会在后面章节中学习。 188 | 189 | ## Map中的新方法 190 | 相比`Collection`,`Map`中加入了更多的方法,我们以`HashMap`为例来逐一探秘。了解[Java7`HashMap`实现原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet%20and%20HashMap.md),将有助于理解下文。 191 | 192 | ### forEach() 193 | 194 | 该方法签名为`void forEach(BiConsumer action)`,作用是**对`Map`中的每个映射执行`action`指定的操作**,其中`BiConsumer`是一个函数接口,里面有一个待实现方法`void accept(T t, U u)`。`BinConsumer`接口名字和`accept()`方法名字都不重要,请不要记忆他们。 195 | 196 | 需求:*假设有一个数字到对应英文单词的Map,请输出Map中的所有映射关系.* 197 | 198 | Java7以及之前经典的代码如下: 199 | 200 | ```Java 201 | // Java7以及之前迭代Map 202 | HashMap map = new HashMap<>(); 203 | map.put(1, "one"); 204 | map.put(2, "two"); 205 | map.put(3, "three"); 206 | for(Map.Entry entry : map.entrySet()){ 207 | System.out.println(entry.getKey() + "=" + entry.getValue()); 208 | } 209 | ``` 210 | 211 | 使用`Map.forEach()`方法,结合匿名内部类,代码如下: 212 | ```Java 213 | // 使用forEach()结合匿名内部类迭代Map 214 | HashMap map = new HashMap<>(); 215 | map.put(1, "one"); 216 | map.put(2, "two"); 217 | map.put(3, "three"); 218 | map.forEach(new BiConsumer(){ 219 | @Override 220 | public void accept(Integer k, String v){ 221 | System.out.println(k + "=" + v); 222 | } 223 | }); 224 | ``` 225 | 上述代码调用`forEach()`方法,并使用匿名内部类实现`BiConsumer`接口。当然,实际场景中没人使用匿名内部类写法,因为有Lambda表达式: 226 | 227 | ```Java 228 | // 使用forEach()结合Lambda表达式迭代Map 229 | HashMap map = new HashMap<>(); 230 | map.put(1, "one"); 231 | map.put(2, "two"); 232 | map.put(3, "three"); 233 | map.forEach((k, v) -> System.out.println(k + "=" + v)); 234 | } 235 | ``` 236 | 237 | ### getOrDefault() 238 | 239 | 该方法跟Lambda表达式没关系,但是很有用。方法签名为`V getOrDefault(Object key, V defaultValue)`,作用是**按照给定的`key`查询`Map`中对应的`value`,如果没有找到则返回`defaultValue`**。使用该方法程序员可以省去查询指定键值是否存在的麻烦. 240 | 241 | 需求;*假设有一个数字到对应英文单词的Map,输出4对应的英文单词,如果不存在则输出NoValue* 242 | 243 | ```Java 244 | // 查询Map中指定的值,不存在时使用默认值 245 | HashMap map = new HashMap<>(); 246 | map.put(1, "one"); 247 | map.put(2, "two"); 248 | map.put(3, "three"); 249 | // Java7以及之前做法 250 | if(map.containsKey(4)){ // 1 251 | System.out.println(map.get(4)); 252 | }else{ 253 | System.out.println("NoValue"); 254 | } 255 | // Java8使用Map.getOrDefault() 256 | System.out.println(map.getOrDefault(4, "NoValue")); // 2 257 | ``` 258 | ### putIfAbsent() 259 | 260 | 该方法跟Lambda表达式没关系,但是很有用。方法签名为`V putIfAbsent(K key, V value)`,作用是只有在**不存在`key`值的映射或映射值为`null`时**,才将`value`指定的值放入到`Map`中,否则不对`Map`做更改.该方法将条件判断和赋值合二为一,使用起来更加方便. 261 | 262 | ### remove() 263 | 264 | 我们都知道`Map`中有一个`remove(Object key)`方法,来根据指定`key`值删除`Map`中的映射关系;Java8新增了`remove(Object key, Object value)`方法,只有在当前`Map`中**`key`正好映射到`value`时**才删除该映射,否则什么也不做. 265 | 266 | ### replace() 267 | 268 | 在Java7及以前,要想替换`Map`中的映射关系可通过`put(K key, V value)`方法实现,该方法总是会用新值替换原来的值.为了更精确的控制替换行为,Java8在`Map`中加入了两个`replace()`方法,分别如下: 269 | 270 | * `replace(K key, V value)`,只有在当前`Map`中**`key`的映射存在时**才用`value`去替换原来的值,否则什么也不做. 271 | * `replace(K key, V oldValue, V newValue)`,只有在当前`Map`中**`key`的映射存在且等于`oldValue`时**才用`newValue`去替换原来的值,否则什么也不做. 272 | 273 | ### replaceAll() 274 | 275 | 该方法签名为`replaceAll(BiFunction function)`,作用是对`Map`中的每个映射执行`function`指定的操作,并用`function`的执行结果替换原来的`value`,其中`BiFunction`是一个函数接口,里面有一个待实现方法`R apply(T t, U u)`.不要被如此多的函数接口吓到,因为使用的时候根本不需要知道他们的名字. 276 | 277 | 需求:*假设有一个数字到对应英文单词的Map,请将原来映射关系中的单词都转换成大写.* 278 | 279 | Java7以及之前经典的代码如下: 280 | 281 | ```Java 282 | // Java7以及之前替换所有Map中所有映射关系 283 | HashMap map = new HashMap<>(); 284 | map.put(1, "one"); 285 | map.put(2, "two"); 286 | map.put(3, "three"); 287 | for(Map.Entry entry : map.entrySet()){ 288 | entry.setValue(entry.getValue().toUpperCase()); 289 | } 290 | ``` 291 | 292 | 使用`replaceAll()`方法结合匿名内部类,实现如下: 293 | 294 | ```Java 295 | // 使用replaceAll()结合匿名内部类实现 296 | HashMap map = new HashMap<>(); 297 | map.put(1, "one"); 298 | map.put(2, "two"); 299 | map.put(3, "three"); 300 | map.replaceAll(new BiFunction(){ 301 | @Override 302 | public String apply(Integer k, String v){ 303 | return v.toUpperCase(); 304 | } 305 | }); 306 | ``` 307 | 上述代码调用`replaceAll()`方法,并使用匿名内部类实现`BiFunction`接口。更进一步的,使用Lambda表达式实现如下: 308 | 309 | ```Java 310 | // 使用replaceAll()结合Lambda表达式实现 311 | HashMap map = new HashMap<>(); 312 | map.put(1, "one"); 313 | map.put(2, "two"); 314 | map.put(3, "three"); 315 | map.replaceAll((k, v) -> v.toUpperCase()); 316 | ``` 317 | 318 | 简洁到让人难以置信. 319 | 320 | ### merge() 321 | 322 | 该方法签名为`merge(K key, V value, BiFunction remappingFunction)`,作用是: 323 | 324 | 1. 如果`Map`中`key`对应的映射不存在或者为`null`,则将`value`(不能是`null`)关联到`key`上; 325 | 2. 否则执行`remappingFunction`,如果执行结果非`null`则用该结果跟`key`关联,否则在`Map`中删除`key`的映射. 326 | 327 | 参数中`BiFunction`函数接口前面已经介绍过,里面有一个待实现方法`R apply(T t, U u)`. 328 | 329 | `merge()`方法虽然语义有些复杂,但该方法的用方式很明确,一个比较常见的场景是将新的错误信息拼接到原来的信息上,比如: 330 | 331 | ```Java 332 | map.merge(key, newMsg, (v1, v2) -> v1+v2); 333 | ``` 334 | 335 | ### compute() 336 | 337 | 该方法签名为`compute(K key, BiFunction remappingFunction)`,作用是把`remappingFunction`的计算结果关联到`key`上,如果计算结果为`null`,则在`Map`中删除`key`的映射. 338 | 339 | 要实现上述`merge()`方法中错误信息拼接的例子,使用`compute()`代码如下: 340 | 341 | ```Java 342 | map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg)); 343 | ``` 344 | 345 | ### computeIfAbsent() 346 | 347 | 该方法签名为`V computeIfAbsent(K key, Function mappingFunction)`,作用是:只有在当前`Map`中**不存在`key`值的映射或映射值为`null`时**,才调用`mappingFunction`,并在`mappingFunction`执行结果非`null`时,将结果跟`key`关联. 348 | 349 | `Function`是一个函数接口,里面有一个待实现方法`R apply(T t)`. 350 | 351 | `computeIfAbsent()`常用来对`Map`的某个`key`值建立初始化映射.比如我们要实现一个多值映射,`Map`的定义可能是`Map>`,要向`Map`中放入新值,可通过如下代码实现: 352 | 353 | ```Java 354 | Map> map = new HashMap<>(); 355 | // Java7及以前的实现方式 356 | if(map.containsKey(1)){ 357 | map.get(1).add("one"); 358 | }else{ 359 | Set valueSet = new HashSet(); 360 | valueSet.add("one"); 361 | map.put(1, valueSet); 362 | } 363 | // Java8的实现方式 364 | map.computeIfAbsent(1, v -> new HashSet()).add("yi"); 365 | ``` 366 | 367 | 使用`computeIfAbsent()`将条件判断和添加操作合二为一,使代码更加简洁. 368 | 369 | ### computeIfPresent() 370 | 371 | 该方法签名为`V computeIfPresent(K key, BiFunction remappingFunction)`,作用跟`computeIfAbsent()`相反,即,只有在当前`Map`中**存在`key`值的映射且非`null`时**,才调用`remappingFunction`,如果`remappingFunction`执行结果为`null`,则删除`key`的映射,否则使用该结果替换`key`原来的映射. 372 | 373 | 这个函数的功能跟如下代码是等效的: 374 | 375 | ```Java 376 | // Java7及以前跟computeIfPresent()等效的代码 377 | if (map.get(key) != null) { 378 | V oldValue = map.get(key); 379 | V newValue = remappingFunction.apply(key, oldValue); 380 | if (newValue != null) 381 | map.put(key, newValue); 382 | else 383 | map.remove(key); 384 | return newValue; 385 | } 386 | return null; 387 | ``` 388 | 389 | 390 | ## 总结 391 | 392 | 1. Java8为容器新增一些有用的方法,这些方法有些是为**完善原有功能**,有些是为**引入函数式编程**,学习和使用这些方法有助于我们写出更加简洁有效的代码. 393 | 2. **函数接口**虽然很多,但绝大多数时候我们根本不需要知道它们的名字,书写Lambda表达式时类型推断帮我们做了一切. 394 | --------------------------------------------------------------------------------