Lambda应用与浅析

引入

在Java8之前创建一个线程的写法(之一):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LambdaTest {



public static void main(String[] args) {

new Thread(new Runnable() {

@Override

public void run() {

System.out.println("hello inner class!");

}

});

}

}

通过查看编译后的生成的Class文件,可以得知new Runnable这块会生成一个匿名类:

文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class LambdaTest$1 implements Runnable {

LambdaTest$1() {

}



public void run() {

System.out.println("hello inner class!");

}

}

在Java8开始一切都开始变得不同,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
public class LambdaTest {



public static void main(String[] args) {

new Thread(()->System.out.println("hello inner class!"));

}

}

其中()->System.out.println("hello inner class!")就是今天的主角lambda表达式!

简介

Lambda表达式,是一种匿名函数,一开始只有函数式编程语言中有此功能,但是现在已经有越来越多的命令式编程语言中也支持了Lambda表达式。

Lambda表达式不是一个新鲜事,最早支持Lambda表达式的是1958发布的LISP语言。在Java中也存在了快7-8年时间(2014年发布的Java8开始支持),目前Java已经都发布16。

Lambda表达式简化匿名内部类的书写,但Lambda表达式并不能取代所有的匿名内部类,只能用来取代函数接口(Functional Interface)的简写。

说到函数接口,那么来看下什么是函数接口,看看最开始的例子里出现的Runnable

1
2
3
4
5
6
7
8
9
@FunctionalInterface

public interface Runnable {



public abstract void run();

}

再看看另外一个Comparable :

1
2
3
4
5
6
7
public interface Comparable<T> {



public int compareTo(T o);

}

发现了什么?只有一个[抽象]方法的接口即是函数接口。

@FunctionalInterface 并不是必须的,加上此注解可以让编译器帮你check当前定义的是不是正确的函数接口:

Java8 引入Lambda表达式的同时也定义了很多新的函数接口,适用于大部分场景:

也就是说Java8中的Lambda表达式不可以随意乱写,必须有与其对应的函数接口定义,例如:

1
(a, b) -> a + b;

如果在JDK中找不到对应的函数接口,我们可以自定义一个:

1
2
3
4
5
6
7
@FunctionalInterface

public interface TwoParamsOneResultFunction {

Integer get(Integer p1, Integer p2);

}

应用

下面来通过一个实例看看Lambda表达式的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private static boolean sync2Es(List<Long> result, EsIndexEnum esIndexEnum, boolean isAsync) {

log.info("同步{}至ES,idList:{},isAsync:{}", JSON.toJSONString(esIndexEnum), JSON.toJSONString(result), isAsync);

if (esIndexEnum == null) {

return false;

}

if (CollectionUtils.isEmpty(result)) {

return true;

}

//es最多支持一次同步3000条,所以需要拆分

List<List<Long>> partition = Lists.partition(StreamUtils.safeList(result), AssetsConstant.MAX_BATCH_SYNC_SIZE);

//es支持同步和异步,按需选择

EsSyncProcessorInstance instance = EsSyncProcessorFactory.getInstance(esIndexEnum.getIndexClass());

BiFunction<EsSyncProcessorInstance, List<Long>, EsResult<BulkMessage>> esFunction = getEsSyncFunction(isAsync);

boolean esResult = StreamUtils.allMatch(StreamUtils.listToList(partition, ids -> {

EsResult<BulkMessage> bulkMessageEsResult = esFunction.apply(instance, ids);

//es可能失败,重试一次,如果还是不行那估计就是大问题了。

if (!bulkMessageEsResult.isSuccess()) {

log.error("同步ES失败:{},重试一次", JSON.toJSONString(bulkMessageEsResult));

bulkMessageEsResult = esFunction.apply(instance, StreamUtils.listToList(StreamUtils.safeGetField(bulkMessageEsResult.getValue(), BulkMessage::getFailIds), Long::valueOf));

if (!bulkMessageEsResult.isSuccess()) {

log.error("同步ES重试再次失败:{}", JSON.toJSONString(bulkMessageEsResult));

}

}

return bulkMessageEsResult.isSuccess();

}), Boolean::booleanValue);

if (!esResult) {

log.error("同步到ES存在失败:{}", JSON.toJSONString(result));

}

return esResult;

}

上面这一段代码主要是在做同步数据给ES的逻辑,由于ES提供了多种同步方式,不想将细节暴露给上层,所以做了一些封装。

先看看这一段:

1
BiFunction<EsSyncProcessorInstance, List<Long>, EsResult<BulkMessage>> esFunction = getEsSyncFunction(isAsync);

调用了:

1
2
3
4
5
private static BiFunction<EsSyncProcessorInstance, List<Long>, EsResult<BulkMessage>> getEsSyncFunction(boolean isAsync) {

return isAsync ? EsSyncProcessorInstance::syncByPrimaryKey : EsSyncProcessorInstance::syncByPrimaryKeyImmediate;

}

其中可以看到类似EsSyncProcessorInstance::syncByPrimaryKey这种写法,这个叫方法引用,可以理解为是一种特殊的lambda表达式(语法糖),相当于给一段函数取了一个名字,然后直接引用,看看syncByPrimaryKey:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public EsResult<BulkMessage> syncByPrimaryKey(List<Long> indexPrimaryKeyList) {

String method = "EsSyncProcessorInstance#syncByPrimaryKey";

if (indexPrimaryKeyList != null && indexPrimaryKeyList.size() > 3000) {

log.warn("{} allowable size exceeded, size={}, max={}", new Object[]{method, indexPrimaryKeyList.size(), 3000});

return null;

} else {

EsSyncLoopQuery query = this.newInstanceQuery();

query.setIdList(indexPrimaryKeyList);

return this.synchronizeToEs(query, this.defaultConfig());

}

}

需要一个List 参数,一个EsResult返回值,那为什么适配的函数接口是BiFunction?看看BiFunction:

1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface

public interface BiFunction<T, U, R> {



//这里需要两个参数

R apply(T t, U u);

...

}

由于syncByPrimaryKey是一个实例方法,所以还需要一个实例对象才能调用,所以EsSyncProcessorInstance::syncByPrimaryKey 相当于(EsSyncProcessorInstance instance,List<Long> ids)->{...}

再一次验证,lambda表达式不能随意乱写,必须有对应的函数接口对应。

接下来再来看看:

1
StreamUtils.listToList(partition, ids -> {

这是对Java8 Stream+Lambda的一层封装:

1
2
3
4
5
public static <T, R> List<R> listToList(List<T> list, Function<T, R> function) {

return safeList(list).stream().map(function).filter(Objects::nonNull).collect(Collectors.toList());

}

可以看到stream().map需要一个function用来转换数据,而其他的基本都是固定写法,这体现了Lambda的另外一个好处:抽象行为。

还有很多用法不一一介绍了。

原理

这么🐂🍺的Lambda表达式Java底层是怎么实现的呢?首先可以确定的是,不是匿名内部类,还是最开始的例子,我们改成lambda表达式后编译看看:

1
2
3
4
5
6
7
8
9
10
11
public class LambdaTest {



public static void main(String[] args) {

new Thread(()->System.out.println("hello lambda"));

}

}

可以看到并没有匿名内部类生成,我们查看下LambdaTest.class字节码(javap -c -p -v LambdaTest.class):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
Classfile /Users/zhaojingzhou/workspace/larry/lambda/src/LambdaTest.class

Last modified 2021-8-2; size 1047 bytes

MD5 checksum 191cb67c3e5a457eb20b4c8a47f5a5d2

Compiled from "LambdaTest.java"

public class LambdaTest

minor version: 0

major version: 52

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref #9.#19 // java/lang/Object."<init>":()V

#2 = Class #20 // java/lang/Thread

#3 = InvokeDynamic #0:#25 // #0:run:()Ljava/lang/Runnable;

#4 = Methodref #2.#26 // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V

#5 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;

#6 = String #29 // hello inner class!

#7 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava/lang/String;)V

#8 = Class #32 // LambdaTest

#9 = Class #33 // java/lang/Object

#10 = Utf8 <init>

#11 = Utf8 ()V

#12 = Utf8 Code

#13 = Utf8 LineNumberTable

#14 = Utf8 main

#15 = Utf8 ([Ljava/lang/String;)V

#16 = Utf8 lambda$main$0

#17 = Utf8 SourceFile

#18 = Utf8 LambdaTest.java

#19 = NameAndType #10:#11 // "<init>":()V

#20 = Utf8 java/lang/Thread

#21 = Utf8 BootstrapMethods

#22 = MethodHandle #6:#34 // 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;

#23 = MethodType #11 // ()V

#24 = MethodHandle #6:#35 // invokestatic LambdaTest.lambda$main$0:()V

#25 = NameAndType #36:#37 // run:()Ljava/lang/Runnable;

#26 = NameAndType #10:#38 // "<init>":(Ljava/lang/Runnable;)V

#27 = Class #39 // java/lang/System

#28 = NameAndType #40:#41 // out:Ljava/io/PrintStream;

#29 = Utf8 hello inner class!

#30 = Class #42 // java/io/PrintStream

#31 = NameAndType #43:#44 // println:(Ljava/lang/String;)V

#32 = Utf8 LambdaTest

#33 = Utf8 java/lang/Object

#34 = Methodref #45.#46 // 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;

#35 = Methodref #8.#47 // LambdaTest.lambda$main$0:()V

#36 = Utf8 run

#37 = Utf8 ()Ljava/lang/Runnable;

#38 = Utf8 (Ljava/lang/Runnable;)V

#39 = Utf8 java/lang/System

#40 = Utf8 out

#41 = Utf8 Ljava/io/PrintStream;

#42 = Utf8 java/io/PrintStream

#43 = Utf8 println

#44 = Utf8 (Ljava/lang/String;)V

#45 = Class #48 // java/lang/invoke/LambdaMetafactory

#46 = NameAndType #49:#53 // 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;

#47 = NameAndType #16:#11 // lambda$main$0:()V

#48 = Utf8 java/lang/invoke/LambdaMetafactory

#49 = Utf8 metafactory

#50 = Class #55 // java/lang/invoke/MethodHandles$Lookup

#51 = Utf8 Lookup

#52 = Utf8 InnerClasses

#53 = 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;

#54 = Class #56 // java/lang/invoke/MethodHandles

#55 = Utf8 java/lang/invoke/MethodHandles$Lookup

#56 = Utf8 java/lang/invoke/MethodHandles

{

public LambdaTest();

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 9: 0



public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)V

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=3, locals=1, args_size=1

0: new #2 // class java/lang/Thread

3: dup

4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;

9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V

12: pop

13: return

LineNumberTable:

line 14: 0

line 19: 13



private static void lambda$main$0();

descriptor: ()V

flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC

Code:

stack=2, locals=0, args_size=0

0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc #6 // String hello inner class!

5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

LineNumberTable:

line 14: 0

}

SourceFile: "LambdaTest.java"

InnerClasses:

public static final #51= #50 of #54; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

BootstrapMethods:

0: #22 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:

#23 ()V

#24 invokestatic LambdaTest.lambda$main$0:()V

#23 ()V

很多信息我们关注一下重点:

1
4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;

第85行可以看到原lambda表达式被编译成了invokedynamic 字节码,这个是Java中多个调用字节码之一,其它的分别是invokestaticinvokevirtualinvokespecialinvokeinterface。也是Java1.0以后第一个被新增的调用字节码。

接下来看#3:

1
#3 = InvokeDynamic      #0:#25         // #0:run:()Ljava/lang/Runnable;

#0:

1
2
3
4
5
6
7
8
9
10
11
BootstrapMethods:

0: #22 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:

#23 ()V

#24 invokestatic LambdaTest.lambda$main$0:()V

#23 ()V

JVM规范规定,如果类的常量池中存在CONSTANT_InvokeDynamic_info的话,那么attributes表中就必定有且仅有一个BootstrapMethods属性。BootstrapMethods属性是个变长的表。

其中根据相关信息会创建一个DynamicCallSite动态调用点,可以看到最终调用的方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
invokestatic LambdaTest.lambda$main$0:()V
private static void lambda$main$0();

descriptor: ()V

flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC

Code:

stack=2, locals=0, args_size=0

0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc #6 // String hello inner class!

5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

LineNumberTable:

line 14: 0

也就是说Java虽然不会对Lambda表达式生成匿名类,但是会生成匿名静态方法。

引用

https://segmentfault.com/a/1190000020607546

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

https://stackoverflow.com/questions/30733557/what-is-a-bootstrap-method

作者

太阳当空赵先生

发布于

2021-08-02

更新于

2022-05-23

许可协议

评论