1. 匿名内部类实现
匿名内部类仍然是一个类, 只是不需要程序员显示指定类名, 编译器会自动为该类取名. 因此如果有如下形式的代码, 编译之后将会产生两个 class 文件:
- public class MainAnonymousClass {
- public static void main(String[] args) {
- new Thread(new Runnable(){
- @Override
- public void run(){
- System.out.println("Anonymous Class Thread run()");
- }
- }).start();;
- }
- }
编译之后文件分布如下, 两个 class 文件分别是主类和匿名内部类产生的:
进一步分析主类 MainAnonymousClass.class 的字节码, 可发现其创建了匿名内部类的对象:
- // javap -c MainAnonymousClass.class
- public class MainAnonymousClass {
- ...
- public static void main(java.lang.String[]);
- Code:
- 0: new #2 // class java/lang/Thread
- 3: dup
- 4: new #3 // class MainAnonymousClass$1 /* 创建内部类对象 */
- 7: dup
- 8: invokespecial #4 // Method MainAnonymousClass$1."<init>":()V
- 11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
- 14: invokevirtual #6 // Method java/lang/Thread.start:()V
- 17: return
- }
2. Lambda 表达式实现
Lambda 表达式通过 invokedynamic 指令实现, 书写 Lambda 表达式不会产生新的类. 如果有如下代码, 编译之后只有一个 class 文件:
- public class MainLambda {
- public static void main(String[] args) {
- new Thread(
- () -> System.out.println("Lambda Thread run()")
- ).start();;
- }
- }
编译之后的结果:
通过 javap 反编译命名, 我们更能看出 Lambda 表达式内部表示的不同:
- // javap -c -p MainLambda.class
- public class MainLambda {
- ...
- public static void main(java.lang.String[]);
- Code:
- 0: new #2 // class java/lang/Thread
- 3: dup
- 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; /* 使用 invokedynamic 指令调用 */
- 9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
- 12: invokevirtual #5 // Method java/lang/Thread.start:()V
- 15: return
- private static void lambda$main$0(); /*Lambda 表达式被封装成主类的私有方法 */
- Code:
- 0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
- 3: ldc #7 // String Lambda Thread run()
- 5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 8: return
- }
反编译之后我们发现 Lambda 表达式被封装成了主类的一个私有方法, 并通过 invokedynamic 指令进行调用.
3. Streams API(I)
你可能没意识到 Java 对函数式编程的重视程度, 看看 Java 8 加入函数式编程扩充多少功能就清楚了. Java 8 之所以费这么大功夫引入函数式编程, 原因有二:
代码简洁函数式编程写出的代码简洁且意图明确, 使用 stream 接口让你从此告别 for 循环.
多核友好, Java 函数式编程使得编写并行程序从未如此简单, 你需要的全部就是调用一下 parallel()方法.
图中 4 种 stream 接口继承自 BaseStream, 其中 IntStream, LongStream, DoubleStream 对应三种基本类型(int, long, double, 注意不是包装类型),Stream 对应所有剩余类型的 stream 视图. 为不同数据类型设置不同 stream 接口, 可以 1. 提高性能, 2. 增加特定接口函数.
虽然大部分情况下 stream 是容器调用 Collection.stream()方法得到的, 但 stream 和 collections 有以下不同:
无存储. stream 不是一种数据结构, 它只是某种数据源的一个视图, 数据源可以是一个数组, Java 容器或 I/O channel 等.
为函数式编程而生. 对 stream 的任何修改都不会修改背后的数据源, 比如对 stream 执行过滤操作并不会删除被过滤的元素, 而是会产生一个不包含被过滤元素的新 stream.
惰式执行. stream 上的操作并不会立即执行, 只有等到用户真正需要结果的时候才会执行.
可消费性. stream 只能被 "消费" 一次, 一旦遍历过就会失效, 就像容器的迭代器那样, 想要再次遍历必须重新生成.
对 stream 的操作分为为两类, 中间操作 (intermediate operations) 和结束操作(terminal operations), 二者特点是:
中间操作总是会惰式执行, 调用中间操作只会生成一个标记了该操作的新 stream, 仅此而已.
结束操作会触发实际计算, 计算发生时会把所有中间操作积攒的操作以 pipeline 的方式执行, 这样可以减少迭代次数. 计算完成之后 stream 就会失效.
如果你熟悉 Apache Spark RDD, 对 stream 的这个特点应该不陌生.
下表汇总了 Stream 接口的部分常见方法:
操作类型 | 接口方法 |
---|---|
中间操作 | concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered() |
结束操作 | allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() |
区分中间操作和结束操作最简单的方法, 就是看方法的返回值, 返回值为 stream 的大都是中间操作, 否则是结束操作.
3.1 flatMap()
函数原型为 < R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper), 作用是对每个元素执行 mapper 指定的操作, 并用所有 mapper 返回的 Stream 中的元素组成一个新的 Stream 作为最终返回结果. 说起来太拗口, 通俗的讲 flatMap()的作用就相当于把原 stream 中的所有元素都 "摊平" 之后组成的 Stream, 转换前后元素的个数和类型都可能会改变.
- Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
- stream.flatMap(list -> list.stream())
- .forEach(i -> System.out.println(i));
上述代码中, 原来的 stream 中有两个元素, 分别是两个 List<Integer>, 执行 flatMap()之后, 将每个 List 都 "摊平" 成了一个个的数字, 所以会新产生一个由 5 个数字组成的 Stream. 所以最终将输出 1~5 这 5 个数字.
截止到目前我们感觉良好, 已介绍 Stream 接口函数理解起来并不费劲儿. 如果你就此以为函数式编程不过如此, 恐怕是高兴地太早了. 下一节对 Stream 规约操作的介绍将刷新你现在的认识.
3.2 多面手 reduce()
reduce 操作可以实现从一组元素中生成一个值, sum(),max(),min(),count()等都是 reduce 操作, 将他们单独设为函数只是因为常用. reduce()的方法定义有三种重写形式:
- Optional<T> reduce(BinaryOperator<T> accumulator)
- T reduce(T identity, BinaryOperator<T> accumulator)
- <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
虽然函数定义越来越长, 但语义不曾改变, 多的参数只是为了指明初始值 (参数 identity), 或者是指定并行执行时多个部分结果的合并方式(参数 combiner).reduce() 最常用的场景就是从一堆值中生成一个值. 用这么复杂的函数去求一个最大或最小值, 你是不是觉得设计者有病. 其实不然, 因为 "大" 和 "小" 或者 "求和" 有时会有不同的语义.
需求: 从一组单词中找出最长的单词. 这里 "大" 的含义就是 "长".
- // 找出最长的单词
- Stream<String> stream = Stream.of("I", "love", "you", "too");
- Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
- //Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
- System.out.println(longest.get());
上述代码会选出最长的单词 love, 其中 Optional 是 (一个) 值的容器, 使用它可以避免 null 值的麻烦. 当然可以使用 Stream.max(Comparator<? super T> comparator)方法来达到同等效果, 但 reduce()自有其存在的理由.
需求: 求出一组单词的长度之和. 这是个 "求和" 操作, 操作对象输入类型是 String, 而结果类型是 Integer.
- // 求单词长度之和
- Stream<String> stream = Stream.of("I", "love", "you", "too");
- Integer lengthSum = stream.reduce(0, // 初始值 // (1)
- (sum, str) -> sum+str.length(), // 累加器 // (2)
- (a, b) -> a+b); // 部分和拼接器, 并行执行时才会用到 // (3)
- // int lengthSum = stream.mapToInt(str -> str.length()).sum();
- System.out.println(lengthSum);
上述代码标号 (2) 处将 i. 字符串映射成长度, ii. 并和当前累加和相加. 这显然是两步操作, 使用 reduce()函数将这两步合二为一, 更有助于提升性能. 如果想要使用 map()和 sum()组合来达到上述目的, 也是可以的.
reduce()擅长的是生成一个值, 如果想要从 Stream 生成一个集合或者 Map 等复杂的对象该怎么办呢? 终极武器 collect()横空出世!
3.3 终极武器 collect()
不夸张的讲, 如果你发现某个功能在 Stream 接口中没找到, 十有八九可以通过 collect()方法实现. collect()是 Stream 接口方法中最灵活的一个, 学会它才算真正入门 Java 函数式编程. 先看几个热身的小例子:
- // 将 Stream 转换成容器或 Map
- Stream<String> stream = Stream.of("I", "love", "you", "too");
- List<String> list = stream.collect(Collectors.toList()); // (1)
- // Set<String> set = stream.collect(Collectors.toSet()); // (2)
- // Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)
上述代码分别列举了如何将 Stream 转换成 List,Set 和 Map. 虽然代码语义很明确, 可是我们仍然会有几个疑问:
Function.identity()是干什么的?
String::length 是什么意思?
Collectors 是个什么东西?
3.3.1 接口的静态方法和默认方法
Function 是一个接口, 那么 Function.identity()是什么意思呢? 这要从两方面解释:
Java 8 允许在接口中加入具体方法. 接口中的具体方法有两种, default 方法和 static 方法, identity()就是 Function 接口的一个静态方法.
Function.identity()返回一个输出跟输入一样的 Lambda 表达式对象, 等价于形如 t -> t 形式的 Lambda 表达式.
上面的解释是不是让你疑问更多? 不要问我为什么接口中可以有具体方法, 也不要告诉我你觉得 t -> t 比 identity()方法更直观. 我会告诉你接口中的 default 方法是一个无奈之举, 在 Java 7 及之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的, 因为所有实现了该接口的类都要重新实现. 试想在 Collection 接口中加入一个 stream()抽象方法会怎样? default 方法就是用来解决这个尴尬问题的, 直接在接口中实现新加入的方法. 既然已经引入了 default 方法, 为何不再加入 static 方法来避免专门的工具类呢!
3.3.2 方法引用
诸如 String::length 的语法形式叫做方法引用(method references), 这种语法用来替代某些特定形式 Lambda 表达式. 如果 Lambda 表达式的全部内容就是调用一个已有的方法, 那么可以用方法引用来替代 Lambda 表达式. 方法引用可以细分为四类:
方法引用类别 | 举例 |
---|---|
引用静态方法 | Integer::sum |
引用某个对象的方法 | list::add |
引用某个类的方法 | String::length |
引用构造方法 | HashMap::new |
我们会在后面的例子中使用方法引用.
3.3.3 收集器
相信前面繁琐的内容已彻底打消了你学习 Java 函数式编程的热情, 不过很遗憾, 下面的内容更繁琐. 但这不能怪 Stream 类库, 因为要实现的功能本身很复杂.
收集器 (Collector) 是为 Stream.collect()方法量身打造的工具接口 (类). 考虑一下将一个 Stream 转换成一个容器(或者 Map) 需要做哪些工作? 我们至少需要两样东西:
目标容器是什么? 是 ArrayList 还是 HashSet, 或者是个 TreeMap.
新元素如何添加到容器中? 是 List.add()还是 Map.put().
如果并行的进行规约, 还需要告诉 collect() 3. 多个部分结果如何合并成一个.
结合以上分析, collect()方法定义为 < R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner), 三个参数依次对应上述三条分析. 不过每次调用 collect()都要传入这三个参数太麻烦, 收集器 Collector 就是对这三个参数的简单封装, 所以 collect()的另一定义为 < R,A> R collect(Collector<? super T,A,R> collector).Collectors 工具类可通过静态方法生成各种常用的 Collector. 举例来说, 如果要将 Stream 规约成 List 可以通过如下两种方式实现:
3.3.4 使用 collect()生成 Collection
前面已经提到通过 collect()方法将 Stream 转换成容器的方法, 这里再汇总一下. 将 Stream 转换成 List 或 Set 是比较常见的操作, 所以 Collectors 工具已经为我们提供了对应的收集器, 通过如下代码即可完成:
- // 将 Stream 转换成 List 或 Set
- Stream<String> stream = Stream.of("I", "love", "you", "too");
- List<String> list = stream.collect(Collectors.toList()); // (1)
- Set<String> set = stream.collect(Collectors.toSet()); // (2)
上述代码能够满足大部分需求, 但由于返回结果是接口类型, 我们并不知道类库实际选择的容器类型是什么, 有时候我们可能会想要人为指定容器的实际类型, 这个需求可通过 Collectors.toCollection(Supplier<C> collectionFactory)方法完成.
- // 使用 toCollection()指定规约容器的类型
- ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
- HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)
上述代码 (3) 处指定规约结果是 ArrayList, 而 (4) 处指定规约结果为 HashSet. 一切如你所愿.
3.3.5 使用 collect()生成 Map
前面已经说过 Stream 背后依赖于某种数据源, 数据源可以是数组, 容器等, 但不能是 Map. 反过来从 Stream 生成 Map 是可以的, 但我们要想清楚 Map 的 key 和 value 分别代表什么, 根本原因是我们要想清楚要干什么. 通常在三种情况下 collect()的结果会是 Map:
使用 Collectors.toMap()生成的收集器, 用户需要指定如何生成 Map 的 key 和 value.
使用
Collectors.partitioningBy()
生成的收集器, 对元素进行二分区操作时用到.
使用
Collectors.groupingBy()
生成的收集器, 对元素做 group 操作时用到.
情况 1: 使用 toMap()生成的收集器, 这种情况是最直接的, 前面例子中已提到, 这是和 Collectors.toCollection()并列的方法. 如下代码展示将学生列表转换成由<学生, GPA > 组成的 Map. 非常直观, 无需多言.
- // 使用 toMap()统计学生 GPA
- Map<Student, Double> studentToGPA =
- students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成 key
- student -> computeGPA(student)));// 如何生成 value
情况 2: 使用 partitioningBy()生成的收集器, 这种情况适用于将 Stream 中的元素依据某个二值逻辑 (满足条件, 或不满足) 分成互补相交的两部分, 比如男女性别, 成绩及格与否等. 下列代码展示将学生分成成绩及格或不及格的两部分.
- // Partition students into passing and failing
- Map<Boolean, List<Student>> passingFailing = students.stream()
- .collect(Collectors.partitioningBy(s -> s.getGrade()>= PASS_THRESHOLD));
情况 3: 使用 groupingBy()生成的收集器, 这是比较灵活的一种情况. 跟 SQL 中的 group by 语句类似, 这里的 groupingBy()也是按照某个属性对数据进行分组, 属性相同的元素会被对应到 Map 的同一个 key 上. 下列代码展示将员工按照部门进行分组:
- // Group employees by department
- Map<Department, List<Employee>> byDept = employees.stream()
- .collect(Collectors.groupingBy(Employee::getDepartment));
以上只是分组的最基本用法, 有些时候仅仅分组是不够的. 在 SQL 中使用 group by 是为了协助其他查询, 比如 1. 先将员工按照部门分组, 2. 然后统计每个部门员工的人数. Java 类库设计者也考虑到了这种情况, 增强版的 groupingBy()能够满足这种需求. 增强版的 groupingBy()允许我们对元素分组之后再执行某种运算, 比如求和, 计数, 平均值, 类型转换等. 这种先将元素分组的收集器叫做上游收集器, 之后执行其他运算的收集器叫做下游收集器(downstream Collector).
- // 使用下游收集器统计每个部门的人数
- Map<Department, Integer> totalByDept = employees.stream()
- .collect(Collectors.groupingBy(Employee::getDepartment,
- Collectors.counting()));// 下游收集器
上面代码的逻辑是不是越看越像 SQL? 高度非结构化. 还有更狠的, 下游收集器还可以包含更下游的收集器, 这绝不是为了炫技而增加的把戏, 而是实际场景需要. 考虑将员工按照部门分组的场景, 如果我们想得到每个员工的名字(字符串), 而不是一个个 Employee 对象, 可通过如下方式做到:
- // 按照部门对员工分布组, 并只保留员工的名字
- Map<Department, List<String>> byDept = employees.stream()
- .collect(Collectors.groupingBy(Employee::getDepartment,
- Collectors.mapping(Employee::getName,// 下游收集器
- Collectors.toList())));// 更下游的收集器
3.3.6 使用 collect()做字符串 join
这个肯定是大家喜闻乐见的功能, 字符串拼接时使用 Collectors.joining()生成的收集器, 从此告别 for 循环. Collectors.joining()方法有三种重写形式, 分别对应三种不同的拼接方式. 无需多言, 代码过目难忘.
- // 使用 Collectors.joining()拼接字符串
- Stream<String> stream = Stream.of("I", "love", "you");
- //String joined = stream.collect(Collectors.joining());// "Iloveyou"
- //String joined = stream.collect(Collectors.joining(","));// "I,love,you"
- String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"
- https://objcoding.com/2019/03/04/lambda/
- [https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/6-Stream Pipelines.md]( Pipelines.md)
来源: http://www.bubuko.com/infodetail-3650847.html