语法糖
- 语法糖(Syntactic),也称语法糖衣,指在计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但更方便程序员使用。简而言之,语法糖然程序更加简洁,有更高的可读性。
- Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。
解语法糖(脱糖)
语法糖的存在主要是方便开发人员使用,但JVM并不支持这些语法糖。因此这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。
Java匿名内部类和转写成Lambda,字节码上的区别
//Lambda写法
class LambdaTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println("hello, lambda");
};
r.run();
}
}
//匿名内部类写法
class LambdaTest2 {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hello lambda");
}
};
r.run();
}
}
- 匿名内部类写法在编译后会而外生成
LambdaTest2$1.class
类:
{
lambda.LambdaTest2();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class lambda/LambdaTest2$1
3: dup
4: invokespecial #3 // Method lambda/LambdaTest2$1."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #4, 1 // InterfaceMethod java/lang/Runnable.run:()V
14: return
LineNumberTable:
line 11: 0
line 17: 8
line 18: 14
}
- Lambda写法在编译后并没有生成额外的类:
{
lambda.LambdaTest();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
LineNumberTable:
line 10: 0
line 13: 6
line 14: 12
}
在invokedynamic
执行完之后,存储栈顶值,入栈。然后调用接口Runnable
的run()
方法。
这里的InvokeDynamic #0后面的#0对应的并不是常量池里的索引,而是一个叫BootstrapMethods
的:
0: #20 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:
#21 ()V
#22 invokestatic lambda/LambdaTest.lambda$main$0:()V
#21 ()V
也就是0:后面的内容,可以看出它是通过invokedynamic
来调用LambdaMetafactory
的metafactory
方法。断点可知:
public static CallSite metafactory(MethodHandles.Lookup caller, //caller: "lambda.LambdaTest"
String invokedName, //invokedName: "run"
MethodType invokedType, //invokedType: "()Runnable"
MethodType samMethodType, //samMethodType: "()void"
MethodHandle implMethod, //implMethod: "MethodHandle()void"
MethodType instantiatedMethodType) //instantiatedMethodType: "()void"
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
invokedynamic
JVM字节码指令集一直比较稳定,一直到JAVA7中才增加了一个invokedynamic
指令,这是JAVA为了实现动态类型语言支持而做的一种改进。但是在JAVA7中并没有提供直接生成invokedynamic
指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic
指令。直到JAVA8的Lambda表达式的出现,invokedynamic
指令的生成,在java中才有了直接的生成方式。
invokedynamic指令所指定的bootstrap方法,编译器置入,java7中要自己提供一个这种静态方法,由asm工具写入字节码中,java8中jdk提供了这样的一个启动方法。JVM在类加载解析时,如果是invokedynamic时,每次都会进行重新解析,解析的时候,会首先执行bootstrap方法,LambdaMetafactory.metafactory方法的前三个参数,会在运行时根据访问类和运行期常量池动态传入,而后三个参数,则为bootstrap静态参数列表传入:
- 泛型擦除后的方法签名及返回值;
- 对应的匿名方法;
- 泛型擦除之前的方法签名及返回值
2调用的方法:lambda$main$0:()
,而LambdaTest
中并没有这个方法,猜测是编译器帮我们生成的,通过javap -p命令:
class lambda.LambdaTest {
lambda.LambdaTest();
public static void main(java.lang.String[]);
private static void lambda$main$0();
}
可以看到LambdaTest
确实多了一个lambda$main$0:()
方法。反射调用这个方法:
class LambdaTest {
public static void main(String[] args) throws Exception{
Runnable r = () -> {
System.out.println("hello, lambda");
};
r.run();
LambdaTest.class.getDeclaredMethod("lambda$main$0").invoke(null);
}
}
“hello lambda”多打印了一次,查看其字节码:
private static synthetic lambda$main$0()V
L0
LINENUMBER 11 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello, lambda"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 12 L1
RETURN
MAXSTACK = 2
MAXLOCALS = 0
对比LambdaTest2
run()方法的字节码:
public run()V
L0
LINENUMBER 14 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello lambda"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 15 L1
RETURN
L2
LOCALVARIABLE this Llambda/LambdaTest2$1; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
完全是一样的,获取静态变量System.out
,"hello lambda"
入栈,然后调用PrintStream.println
方法。
那么这个lambda$main$0()
是在哪里被调用的?两种方式:
- 抛异常打印堆栈信息
- 借鉴Class.forName的做法——通过Reflection的getCallerClass来获取到调用者的Class信息:
public class LambdaTest {
public static void main(String[] args) throws Exception {
Runnable r = () -> {
try {
Method getCallerClass = Class.forName("sun.reflect.Reflection").getDeclaredMethod("getCallerClass", int.class);
int index = 0;
Class<?> callerClass;
while ((callerClass = (Class<?>) getCallerClass.invoke(null, index)) != null) {
System.out.println(callerClass);
index++;
}
} catch (Exception e) {
e.printStackTrace();
}
};
r.run();
}
}
运行后打印可知调用lambda$main$0()
方法的是一个叫lambda.LambdaTest$$Lambda$1/1531448569
的类
但我们在编译后并没有生成这个类,那可以推测它是JVM在运行时动态生成和加载的Runnable的实现类。打印Runnable实例的类名:
System.out.println(String.format("Runnable Class: %s", r.getClass().getSimpleName()));
打印结果:
hello, lambda
class sun.reflect.Reflection
class lambda.LambdaTest
class lambda.LambdaTest$$Lambda$1/1531448569
class lambda.LambdaTest
Runnable Class: LambdaTest$$Lambda$1/1531448569
- 结论:Lambda表达式在编译后,其函数体都会放到CallerClass里一个额外生成的方法内(所谓的”匿名”函数),由JVM动态生成的实现类去调用。最终它也是通过内部类的方式实现的,只不过不像直接使用内部类那样在编译后生成额外的类,它把生成实现类这一步放到了运行时,由JVM完成。
Java中的Lambda与Android中的Lambda有什么不同?
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Runnable r = () -> {
Log.d("MainActivity", "hello lambda");
};
r.run();
}
}
编译后打开dex:
MainActivity
<linit>()
void lambda$onCreate$0()
void onCreate(android.os.Bundle)
void setContentView(int)
-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA
<clinit>()
<init>()
void run()
com.hxs.androiddesugardemo.-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA INSTANCE
发现此时这个类已经存在dex里面了,同时MainActivity
也多了一个lambda$onCreate$0方法。查看这个方法的字节码:
.method static synthetic lambda$onCreate$0()V
.registers 2
.line 15
const-string v0, "MainActivity"
const-string v1, "hello lambda"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 16
new-instance v0, Ljava/lang/RuntimeException;
invoke-direct {v0}, Ljava/lang/RuntimeException;-><init>()V
throw v0
.end method
- 先注册了2个寄存器;
- 把字符串常量 “Application” 赋值给寄存器v0;
- 把字符串常量 “hello, lambda” 赋值给寄存器v1;
- 调用android.util.Log的静态方法d,把寄存器v0和v1的值传了进去(Log.d(“Application”, “hello, lambda”););
- 创建java.lang.RuntimeException对象实例,并赋值到寄存器v0上;
- 调用java.lang.RuntimeException的
()方法(即无参构造函数),目标对象是寄存器v0储存的值; - 抛出异常,目标对象是寄存器v0储存的值(throw new RuntimeException(););
这里和Java的处理方式是一样的,将Lambda主体移到了一个额外生成的方法里。
查看onCreate()方法:
method protected onCreate(Landroid/os/Bundle;)V
.registers 3
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.line 12
invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
.line 13
const v0, 0x7f0b001c
invoke-virtual {p0, v0}, Lcom/hxs/androiddesugardemo/MainActivity;->setContentView(I)V
.line 14
sget-object v0, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;->INSTANCE:Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;
.line 18
.local v0, "r":Ljava/lang/Runnable;
invoke-interface {v0}, Ljava/lang/Runnable;->run()V
.line 19
return-void
.end method
第14行,获取那个自动生成的实现类的静态变量==INSTANCE==,它的类型是这个类本身,也就是说INSTANCE是自动生成的实现类的实例,并赋值到寄存器v0上;
第18行,声明局部变量r,类型是java/lang/Runnable,初始值是寄存器**v0的值,然后调用java/lang/Runnable的借口方法run(),目标对象是寄存器v0存储的值。
这里把原来的【创建对象实例】改成了【获取静态字段】。其实这只是一个优化,因为lambda函数体没有依赖CallerClass的变量或方法,完全可以作为一个静态变量存在,这样能避免反复创建对象
查看这个实现类的字节码:
.class public final synthetic Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;
.super Ljava/lang/Object;
.source "lambda"
# interfaces
.implements Ljava/lang/Runnable;
# static fields
.field public static final synthetic INSTANCE:Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;
# direct methods
.method static synthetic constructor
.registers 1
new-instance v0, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;
invoke-direct {v0}, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;-><init>()V
sput-object v0, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;->INSTANCE:Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$NKcUHI3kbmdlSYn5oJ5mHUINomA;
return-void
.end method
.method private synthetic constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final run()V
.registers 1
invoke-static {}, Lcom/hxs/androiddesugardemo/MainActivity;->lambda$onCreate$0()V
return-void
.end method
在类被加载的时候,就创建了他自己的对象实例并赋值给静态变量INSTANCE。实现的run()方法,里面也是直接调用MainActivity的静态方法lambda$onCreate$0。
如果Lambda表达式调用了CallerClass的变量呢?
将代码稍微改造下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String tip = "hello lambda";
Runnable r = () -> {
Log.d("MainActivity", tip);
throw new RuntimeException();
};
r.run();
}
}
查看**onCreate()**方法的字节码:
.method protected onCreate(Landroid/os/Bundle;)V
.registers 4
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.line 12
invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
.line 13
const v0, 0x7f0b001c
invoke-virtual {p0, v0}, Lcom/hxs/androiddesugardemo/MainActivity;->setContentView(I)V
.line 14
const-string v0, "hello lambda"
.line 15
.local v0, "tip":Ljava/lang/String;
new-instance v1, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$f0md4yi5o91GRldVVHmjjcHcul8;
.local v1, "r":Ljava/lang/Runnable;
invoke-direct {v1, v0}, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$f0md4yi5o91GRldVVHmjjcHcul8;-><init>(Ljava/lang/String;)V
.line 19
invoke-interface {v1}, Ljava/lang/Runnable;->run()V
.line 20
return-void
.end method
第15行,对比之前获取静态变量INSTANCE,这里变成了new-instace构造这个实现类的实例
查看该实现类的字节码:
.class public final synthetic Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$f0md4yi5o91GRldVVHmjjcHcul8;
.super Ljava/lang/Object;
.source "lambda"
# interfaces
.implements Ljava/lang/Runnable;
# instance fields
.field public final synthetic f$0:Ljava/lang/String;
# direct methods
.method public synthetic constructor
.registers 2
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$f0md4yi5o91GRldVVHmjjcHcul8;->f$0:Ljava/lang/String;
return-void
.end method
# virtual methods
.method public final run()V
.registers 2
iget-object v0, p0, Lcom/hxs/androiddesugardemo/-$$Lambda$MainActivity$f0md4yi5o91GRldVVHmjjcHcul8;->f$0:Ljava/lang/String;
invoke-static {v0}, Lcom/hxs/androiddesugardemo/MainActivity;->lambda$onCreate$0(Ljava/lang/String;)V
return-void
.end method
已经没有加载时构建实例对象的代码了,不过实现的run()方法还是一致的。
结论
Andorid的Lambda在打包成dex时会直接生成对应的实现类,相比于java的invokedynamic
这种方式,不同的只是==生成实现类的时机==,这跟直接使用匿名内部类的效基本没有区别。
其实在Lambda编译成class时,还是按照Java原本的方式(invokedynamic
)处理的,查看class文件:
public class com.hxs.androiddesugardemo.MainActivity extends androidx.appcompat.app.AppCompatActivity {
public com.hxs.androiddesugardemo.MainActivity();
Code:
0: aload_0
1: invokespecial #1 // Method androidx/appcompat/app/AppCompatActivity."<init>":()V
4: return
protected void onCreate(android.os.Bundle);
Code:
0: aload_0
1: aload_1
2: invokespecial #2 // Method androidx/appcompat/app/AppCompatActivity.onCreate:(Landroid/os/Bundle;)V
5: aload_0
6: ldc #4 // int 2131427356
8: invokevirtual #5 // Method setContentView:(I)V
11: ldc #6 // String hello lambda
13: astore_2
14: aload_2
15: invokedynamic #7, 0 // InvokeDynamic #0:run:(Ljava/lang/String;)Ljava/lang/Runnable;
20: astore_3
21: aload_3
22: invokeinterface #8, 1 // InterfaceMethod java/lang/Runnable.run:()V
27: return
}
脱糖这一步是由D8编译器去做的,desugarDebug(Release)FileDependencies和**dexBuilderDebug(Release)**,前者是把项目依赖的所有jar包脱糖,后者是将项目自己的class脱糖。
D8脱糖
使用Javac将Java8.java编译成.class
class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
sayHi(s -> System.out.println(s));
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
}
在使用 javac 编译之后,使用 dx 直接运行会报错:
MiracledeMacBook-Pro:d8 miracle$ ~/Library/Android/sdk/build-tools/28.0.3/dx --dex --output . *.class
Uncaught translation error: com.android.dx.cf.code.SimException:
ERROR in Java8.main:([Ljava/lang/String;)V:
invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13)
因为 lambda 的实现使用到了 Java 7 新增加的字节码 invokedynamic. 正如报错信息提示的那样,Android 对这个字节码的支持是在 API 26(8.0) 以上才实现的。因此android有一个名为desugaring的编译流程,它将lambda转换为所有API都兼容的形式。
desugaring的目标?
让新的语法糖可以运行在所有设备上
desugaring的历史
我们使用一款名为 Retrolambda 来实现相关的功能。它使用 JVM 的内建机制,在运行时而不是编译时将 lambda 转换为类的实现。生成新的类很容易增加方法数,但是如果权衡利弊这些成本还是可以接受的 (but work on the tool over time reduced the cost to something reasonable).
随后 Android 的工具链团队发布了一款新的编译器,称其可以将 Java 8 的语法糖脱糖的同时还兼备更好的性能。这款编译器是基于 Eclipse 的 Java 编译器开发的,但是目标是 Dalvik 字节码而不是 Java 字节码。这个版本的 Java 8 的脱糖实现代价高昂,并且使用率低、性能差,与其他工具链不兼容。
当上述的新的编译器最终被弃用时(感谢),一款新的将 Java 字节码翻译到 Java 字节码的脱糖转换器被集成到了 Android Gradle Plugin 中,它实际上源自 Google 自己的构建工具 Bazel. 其脱糖过程挺高效的,但是性能表现仍然不是很理想。事实上它是一个渐进式的解决方案,不停地在寻找更好的解决方案。
随后 D8 发布了,被用来取代传统的 dx 工具链,承诺在 dex 过程中脱糖而不是使用标准的 Java 字节码做转换。相对于 dx 而言,D8 在性能上取得了巨大的成功,并且带来了更高效的脱糖字节码。从 Android Gradle Plugin 3.1 版本开始,D8 成为了默认的 dex 工具,在 3.2 版本开始负责脱糖。
然后使用D8将上述例子编译为.dex:
java -jar d8.jar --lib /Users/miracle/Library/Android/sdk/platforms/android-28/android.jar
--release
--output . *.class
发现编译成功了:
Java8$Logger.class
Java8.java
Java8.class
classes.dex
然后使用Android SDK 中提供的 dexdump 来查看classes.dex中的字节码: /Users/miracle/Library/Android/sdk/build-tools/28.0.3/dexdump -d classes.dex:
(分析流程同上)
SYNTHETIC 标志相关类和方法是被生成的
lambda 代码块存在于原来的类的内部的原因在于,它可能需要访问该类的私有成员变量,而生成的类却是访问不到的。.
原生的Lambda
当配合 –min-api 26 参数编译的话,它会假定你将使用原生的lambda实现而不会进行脱糖:
java -jar d8.jar --lib /Users/miracle/Library/Android/sdk/platforms/android-28/android.jar
--release
--min-api 26
--output . *.class
然而查看生成的.dex文件,还是会发现-$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY 的类被生成了。是bug吗?
原因是java原生的指令invokedynamic的处理是借助java.lang.invoke.LambdaMetafactory 这个类中的名为 metafactory 的方法在运行时即时创建lambda相关的匿名类。而在android的运行时库中(android.jar)其实并没有这个类,android实现了与invokedynamic相同效果的字节码支持,但jdk内建的LambdaMetafactory并不可用。所以D8将这个匿名类的生成放到了编译期,然后使用invokedynamic
和invoke-custom
这两条指令去执行。
这篇文章对这两条指令做了解释,不太懂,大概意思是google为了支持更多的动态语言特性?
Method Referencens(方法引用)
Java8中另一个语法糖,作为lambda的补充,使得创建lambda去指向一个已有的方法的操作变得高效。
public static void main(String... args) {
- sayHi(s -> System.out.println(s));
+ sayHi(System.out::println);
}
使用 javac 编译后,再用 D8 处理,我们会发现与之前的 lambda 有一处显著的不同,会发现生成的 lambda 类的代码块被改变了:
|[00023c] -..Lambda.teOjDu261Kz9uXGt1wlPvIP5S04.log:(Ljava/lang/String;)V
|0000: iget-object v0, v0, L-$$Lambda$teOjDu261Kz9uXGt1wlPvIP5S04;.f$0:Ljava/io/PrintStream; // field@0000
|0002: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0006
|0005: return-void
与之前调用生成的Java8.lambda$main$0类中包含System.out.print()方法不同,log的实现直接调用了System.out.print()
生成的 lambda 类不再是静态单例。字节序 0000 直接读取了 PrintStream 的实例引用,该引用即是 System.out, 它在 main 方法中被调用,并且被传递给相应的构造器(名为
将其进行源码级别的转换:
public static void main(String... args) {
- sayHi(System.out::println);
+ sayHi(new -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(System.out));
}
@@
}
+
+class -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM implements Java8.Logger {
+ private final PrintStream ps;
+
+ -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(PrintStream ps) {
+ this.ps = ps;
+ }
+
+ @Override public void log(String s) {
+ ps.println(s);
+ }
+}
Interface Methods
Java 8 另一个显著的特性是可以在接口中定义静态方法和默认方法。接口中的静态方法可以被用来提供相关的工厂方法,或者其他有助于操作接口的方法。接口中的默认方法则允许你给已有接口中添加默认的方法实现,同时保持兼容性(你不需要给所有实现了该接口的类再全部实现一个新的方法)
这两种语法糖在 API 24 以上都是使用的原生实现。因此不像 lambda 和方法引用,–min-api 24 不会触发 D8 的脱糖操作
Android’s Java 8 Support - Jake Wharton.