lambda从入门到精通
JDK8中包含了许多内建的Java中常用到函数接口,比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在lambda上。
name
|
type
|
description
|
Consumer
|
Consumer< T >
|
接收T对象,不返回值
|
Predicate
|
Predicate< T >
|
接收T对象并返回boolean
|
Function
|
Function< T, R >
|
接收T对象,返回R对象
|
Supplier
|
Supplier< T >
|
提供T对象(例如工厂),不接收值
|
UnaryOperator
|
UnaryOperator
|
接收T对象,返回T对象
|
BinaryOperator
|
BinaryOperator
|
接收两个T对象,返回T对象
|
标注为@FunctionalInterface的接口是函数式接口,该接口只有一个自定义方法。注意,只要接口只包含一个抽象方法,编译器就默认该接口为函数式接口。lambda表示是一个命名方法,将行为像数据一样进行传递。
Collection中的新方法
该方法的签名为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,其中Consumer是个函数接口,里面只有一个待实现方法void accept(T t)。注意,这里的Consumer不重要,只需要知道它是一个函数式接口即可,一般使用不会看见Consumer的身影。
list.forEach(item -> System.out.println(item));
该方法签名为boolean removeIf(Predicate<? super E> filter),作用是删除容器中所有满足filter指定条件的元素,其中Predicate是一个函数接口,里面只有一个待实现方法boolean test(T t),同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。
// list中元素类型String list.removeIf(item -> item.length() < 2);
List.replaceAll()
该方法签名为void replaceAll(UnaryOperator<E> operator),作用是对每个元素执行operator指定的操作,并用操作结果来替换原来的元素。
// list中元素类型String list.replaceAll(item -> item.toUpperCase());
List.sort()
该方法定义在List接口中,方法签名为void sort(Comparator<? super E> c),该方法根据c指定的比较规则对容器元素进行排序。Comparator接口我们并不陌生,其中有一个方法int compare(T o1, T o2)需要实现,显然该接口是个函数接口。
// List.sort()方法结合Lambda表达式 ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); list.sort((str1, str2) -> str1.length()-str2.length());
Map.forEach()
该方法签名为void forEach(BiConsumer<? super K,? super V> action),作用是对Map中的每个映射执行action指定的操作,其中BiConsumer是一个函数接口,里面有一个待实现方法void accept(T t, U u)。
map.forEach((key, value) -> System.out.println(key + ": " + value));
Stream API
认识了几个Java8 Collection新增的几个方法,在了解下Stream API,你会发现它在集合数据处理方面的强大作用。常见的Stream接口继承关系图如下:
- 调用Collection.stream()或者Collection.parallelStream()方法
- 调用Arrays.stream(T[] array)方法
- 无存储。stream不是一种数据结构,它只是某种数据源的一个视图。本质上stream只是存储数据源中元素引用的一种数据结构,注意stream中对元素的更新动作会反映到其数据源上的。
- 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
- 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
对Stream的操作分为2种,中间操作与结束操作,二者的区别是,前者是惰性执行,调用中间操作只会生成一个标记了该操作的新的stream而已;后者会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后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方法
forEach()
stream的遍历操作。
filter()
函数原型为Stream<T> filter(Predicate<? super T> predicate),作用是返回一个只包含满足predicate条件元素的Stream。
distinct()
函数原型为Stream<T> distinct(),作用是返回一个去除重复元素之后的Stream。
sorted()
排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为Stream<T> sorted()和Stream<T> sorted(Comparator<? super T> comparator)。
map()
函数原型为<R> Stream<R> map(Function<? super T,? extends R> mapper),作用是返回一个对当前所有元素执行执行mapper之后的结果组成的Stream。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。
List<Integer> list = CollectionUtil.newArrayList(1, 2, 3, 4); list.stream().map(item -> String.valueOf(item)).forEach(System.out::println); flapmap()
和map类似,不同的是每个元素转换得到的是stream对象,会把子stream对象压缩到父集合中。
List<List<String>> list3 = Arrays.asList( Arrays.asList("aaa", "bb", "ccc"), Arrays.asList("aa", "bbb", "ccc")); list3.stream().flatMap(Collection::stream).collect(Collectors.toList());
reduce 和 collect
reduce的作用是从stream中生成一个值,sum()、max()、min()、count()等都是reduce操作,将他们单独设为函数只是因为常用。
// 找出最长的单词 Stream<String> stream = Stream.of("I", "love", "you", "too"); Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
collect方法是stream中重要的方法,如果某个功能没有在Stream接口中找到,则可以通过collect方法实现。
// 将Stream转换成容器或Map Stream<String> stream = Stream.of("I", "love", "you", "too"); List<String> list = stream.collect(Collectors.toList()); // Set<String> set = stream.collect(Collectors.toSet()); // Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));
诸如String::length的语法形式称为方法引用,这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类。引用静态方法 Integer::sum,引用某个对象的方法 list::add,引用某个类的方法 String::length,引用构造方法 HashMap::new。
Stream Pipelines原理
ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.stream() .filter(s -> s.length() > 1) .map(String::toUpperCase) .sorted() .forEach(System.out::println);
上面的代码和下面的功能一样,不过下面的代码便于打断点调试。
ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.stream() .filter(s -> { return s.length() > 1; }) .map(s -> { return s.toUpperCase(); }) .sorted() .forEach(s -> { System.out.println(s); });
首先filter方法了解一下:
// ReferencePipeline @Override public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) { Objects.requireNonNull(predicate); return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) { // 生成state对应的Sink实现 @Override Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) { return new Sink.ChainedReference<P_OUT, P_OUT>(sink) { @Override public void begin(long size) { downstream.begin(-1); } @Override public void accept(P_OUT u) { if (predicate.test(u)) downstream.accept(u); } }; } }; }
filter方法返回一个StatelessOp实例,并实现了其opWrapSink方法,可以肯定的是opWrapSink方法在之后某个时间点会被调用,进行Sink实例的创建。从代码中可以看出,filter方法不会进行真正的filter动作(也就是遍历列表进行filter操作)。
filter方法中出现了2个新面孔,StatelessOp和Sink,既然是新面孔,那就先认识下:
abstract class AbstractPipeline<E_IN, E_OUT, S extends BaseStream<E_OUT, S>> extends PipelineHelper<E_OUT> implements BaseStream<E_OUT, S>
StatelessOp继承自AbstractPipeline,lambda的流处理可以分为多个stage,每个stage对应一个AbstractPileline和一个Sink。
Stream流水线组织结构示意图如下:
图中通过Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。
Stream上的所有操作分为两类:中间操作和结束操作,中间操作只是一种标记,只有结束操作才会触发实际计算。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful),无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果。
有了AbstractPileline,就可以把整个stream上的多个处理操作(filter/map/…)串起来,但是这只解决了多个处理操作记录的问题,还需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单,只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。这就需要Sink接口了,Sink包含的方法如下:
方法名
|
作用
|
void begin(long size)
|
开始遍历元素之前调用该方法,通知Sink做好准备。
|
void end()
|
所有元素遍历完成之后调用,通知Sink没有更多的元素了。
|
boolean cancellationRequested()
|
是否可以结束操作,可以让短路操作尽早结束。
|
void accept(T t)
|
遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。
|
有了上面的协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink的begin()和end()方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个存放结果的容器,而accept()方法负责将元素添加到该容器,最后end()负责对容器进行排序。Sink的四个接口方法常常相互协作,共同完成计算任务。实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法。
回到最开始地方的代码示例,map/sorted方法流程大致和filter类似,这些操作都是中间操作。重点关注下forEach方法:
// ReferencePipeline @Override public void forEach(Consumer<? super P_OUT> action) { evaluate(ForEachOps.makeRef(action, false)); } // ... -> // AbstractPipeline @Override final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) { copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator); return sink; } @Override final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) { // 各个pipeline的opWrapSink方法回调 for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) { sink = p.opWrapSink(p.previousStage.combinedFlags, sink); } return (Sink<P_IN>) sink; } @Override final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) { if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) { // sink各个方法的回调 wrappedSink.begin(spliterator.getExactSizeIfKnown()); spliterator.forEachRemaining(wrappedSink); wrappedSink.end(); } else { copyIntoWithCancel(wrappedSink, spliterator); } }
forEach()流程中会触发各个Sink的操作,也就是执行各个lambda表达式里的逻辑了。到这里整个lambda流程也就完成了。
Java lambda 原理
Java lambda 一眼看上去有点像匿名内部类的简化形式,但是二者确有着本质的差别。匿名内部类经编译后会生成对应的class文件,格式为XXX$n.class;而lambda代码经过编译后生成一个private方法,方法名格式为lambda$main$n。
// Application.main 方法中代码 ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }); list.forEach(System.out::println);
以上代码就会产生一个Application$1.class文件和一个lambda$main$0的方法。既然lambda实现不是内部类,那么在lambda中this就代表的当前所在类实例。
// Application.main 方法中代码 ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.forEach(item -> { System.out.println(item); });
通过javap -c -p Application.class查看以上代码对应的字节码:
Constant pool: #1 = Methodref #12.#36 // java/lang/Object."<init>":()V #2 = Class #37 // java/lang/String #3 = String #38 // I #4 = String #39 // love #5 = String #40 // you #6 = Methodref #41.#42 // cn/hutool/core/collection/CollectionUtil.newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList; #7 = InvokeDynamic #0:#48 // #0:accept:()Ljava/util/function/Consumer; #8 = Methodref #49.#50 // java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V #9 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream; #10 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V #11 = Class #55 // com/luo/demo/Application #12 = Class #56 // java/lang/Object #13 = Utf8 <init> #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 LocalVariableTable #18 = Utf8 this #19 = Utf8 Lcom/luo/demo/Application; #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Utf8 args #23 = Utf8 [Ljava/lang/String; #24 = Utf8 list #25 = Utf8 Ljava/util/ArrayList; #26 = Utf8 LocalVariableTypeTable #27 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>; #28 = Utf8 lambda$main$0 #29 = Utf8 (Ljava/lang/String;)V #30 = Utf8 item #31 = Utf8 Ljava/lang/String; #32 = Utf8 SourceFile #33 = Utf8 Application.java #34 = Utf8 RuntimeVisibleAnnotations #35 = Utf8 Lorg/springframework/boot/autoconfigure/SpringBootApplication; #36 = NameAndType #13:#14 // "<init>":()V #37 = Utf8 java/lang/String #38 = Utf8 I #39 = Utf8 love #40 = Utf8 you #41 = Class #57 // cn/hutool/core/collection/CollectionUtil #42 = NameAndType #58:#59 // newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList; #43 = Utf8 BootstrapMethods #44 = MethodHandle #6:#60 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #45 = MethodType #61 // (Ljava/lang/Object;)V #46 = MethodHandle #6:#62 // invokestatic com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V #47 = MethodType #29 // (Ljava/lang/String;)V #48 = NameAndType #63:#64 // accept:()Ljava/util/function/Consumer; #49 = Class #65 // java/util/ArrayList #50 = NameAndType #66:#67 // forEach:(Ljava/util/function/Consumer;)V #51 = Class #68 // java/lang/System #52 = NameAndType #69:#70 // out:Ljava/io/PrintStream; #53 = Class #71 // java/io/PrintStream #54 = NameAndType #72:#29 // println:(Ljava/lang/String;)V #55 = Utf8 com/luo/demo/Application #56 = Utf8 java/lang/Object #57 = Utf8 cn/hutool/core/collection/CollectionUtil #58 = Utf8 newArrayList #59 = Utf8 ([Ljava/lang/Object;)Ljava/util/ArrayList; #60 = Methodref #73.#74 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #61 = Utf8 (Ljava/lang/Object;)V #62 = Methodref #11.#75 // com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V #63 = Utf8 accept #64 = Utf8 ()Ljava/util/function/Consumer; #65 = Utf8 java/util/ArrayList #66 = Utf8 forEach #67 = Utf8 (Ljava/util/function/Consumer;)V #68 = Utf8 java/lang/System #69 = Utf8 out #70 = Utf8 Ljava/io/PrintStream; #71 = Utf8 java/io/PrintStream #72 = Utf8 println #73 = Class #76 // java/lang/invoke/LambdaMetafactory #74 = NameAndType #77:#81 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #75 = NameAndType #28:#29 // lambda$main$0:(Ljava/lang/String;)V #76 = Utf8 java/lang/invoke/LambdaMetafactory #77 = Utf8 metafactory #78 = Class #83 // java/lang/invoke/MethodHandles$Lookup #79 = Utf8 Lookup #80 = Utf8 InnerClasses #81 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #82 = Class #84 // java/lang/invoke/MethodHandles #83 = Utf8 java/lang/invoke/MethodHandles$Lookup #84 = Utf8 java/lang/invoke/MethodHandles { public com.luo.demo.Application(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/luo/demo/Application; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=2, args_size=1 0: iconst_3 1: anewarray #2 // class java/lang/String 4: dup 5: iconst_0 6: ldc #3 // String I 8: aastore 9: dup 10: iconst_1 11: ldc #4 // String love 13: aastore 14: dup 15: iconst_2 16: ldc #5 // String you 18: aastore 19: invokestatic #6 // Method cn/hutool/core/collection/CollectionUtil.newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList; 22: astore_1 23: aload_1 24: invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer; 29: invokevirtual #8 // Method java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V 32: return LineNumberTable: line 15: 0 line 16: 23 line 19: 32 LocalVariableTable: Start Length Slot Name Signature 0 33 0 args [Ljava/lang/String; 23 10 1 list Ljava/util/ArrayList; LocalVariableTypeTable: Start Length Slot Name Signature 23 10 1 list Ljava/util/ArrayList<Ljava/lang/String;>; private static void lambda$main$0(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=1, args_size=1 0: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return LineNumberTable: line 17: 0 line 18: 7 LocalVariableTable: Start Length Slot Name Signature 0 8 0 item Ljava/lang/String; }
View Code
通过字节码可以看出,调用lambda方法时使用了invokedynamic,该字节码命令是为了支持动态语言特性而在Java7中新增的。Java的lambda表达式实现上也就借助于invokedynamic命令。
字节码中每一处含有invokeDynamic指令的位置都称为“动态调用点”,这条指令的第一个参数不再是代表方法调用符号引用的CONSTANT_Methodref_info常亮,而是变成为JDK7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。
从上述mian方法的字节码可见,有一个invokeDynamic指令,他的参数为第7项常量(第二个值为0的参数HotSpot中用不到,占位符):
invokedynamic #7, 0 // InvokeDynamic #0:accept ()Ljava/util/function/Consumer;
常量池中第7项是#7 = InvokeDynamic #0:#48 // #0:accept:()Ljava/util/function/Consumer;,说明它是一项CONSTANT_InvokeDynamic_info常量,常量值中前面的#0表示引导方法取BootstrapMethods属性表的第0项,而后面的#48表示引用第48项类型为CONSTANAT_NameAndType_info的常量,从这个常量中可以获取方法名称和描述符,即accept方法。
BootstrapMethods: 0: #44 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #45 (Ljava/lang/Object;)V #46 invokestatic com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V #47 (Ljava/lang/String;)V
上图是在lambda代码中打断点时的调用栈信息,如果在这里的lambda中打印当前所属class,就是Application类,也印证了前面分析的lambda代码会生成一个private方法。
从调用栈的信息来看,是在accept方法中调用lambda对应的private方法(ambda$main$0)的,但是这里的accept方法是属于什么对象呢?从图中看是一串数字字符串,这里可以理解成一个Consumer接口的实现类即可,每个lambda表达式可以理解成在一个新的Consumer实现类中调用的即可。使用命令jmap -histo查看JVM进程类和对象信息可以看到这一行信息:
600: 1 16 com.luo.demo.Application$$Lambda$5/1615039080
参考资料: