引入
在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表达式不可以随意乱写,必须有与其对应的函数接口定义,例如:
如果在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中多个调用字节码之一,其它的分别是invokestatic
、invokevirtual
、invokespecial
、invokeinterface
。也是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