在学习java字节码的时候,会发现两个神奇的现象:
- 1 java匿名类,会在编译期创建一个类似
宿主类名$1.class
这样的文件,并且如果要用instrumentation
去替换原始类的时候,会发现如果新加了匿名类是不允许的。 - 2 lambda的代码是不会在编译期生成匿名类的文件的,它使用的是
invokeDynamic
字节码指令。
匿名类比较简单,从字节码来看他就是个内部类。
但lambda就复杂了,在字节码中会发现3个不同,首先是多了一个private static
的lambda$宿主方法名$0
这样的方法,这个方法的返回值类型就是lambda
返回值类型。第二就是也有一个innerClass
但是这个内部类并不是宿主类名$1
后面会讲。第三个就是BootstrapMethods
引导方法,他的主要作用就是配合invokeDynamic
来找到动态调用点。
匿名类
下面我们详细看一下匿名类,下面是一个会从上下文中捕捉变量的匿名类,他更具有代表性。
public class Anonymous {
private static int field = 10;
public static void main(String[] args) {
int i = 1;
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(i + field);
}
};
r.run(); // 打印11
}
}
上面已经初步介绍了匿名类是编译期生成class
文件的,我们看一下上面这段代码对应的字节码
public class test/Anonymous {
// compiled from: Anonymous.java
// access flags 0x8
static INNERCLASS test/Anonymous$1 null null
// access flags 0xA
private static I field
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Ltest/Anonymous; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 6 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 7 L1
NEW test/Anonymous$1
DUP
ILOAD 1
INVOKESPECIAL test/Anonymous$1.<init> (I)V
ASTORE 2
L2
LINENUMBER 13 L2
ALOAD 2
INVOKEINTERFACE java/lang/Runnable.run ()V (itf)
L3
LINENUMBER 14 L3
RETURN
L4
LOCALVARIABLE args [Ljava/lang/String; L0 L4 0
LOCALVARIABLE i I L1 L4 1
LOCALVARIABLE r Ljava/lang/Runnable; L2 L4 2
MAXSTACK = 3
MAXLOCALS = 3
// access flags 0x1008
static synthetic access$000()I
L0
LINENUMBER 3 L0
GETSTATIC test/Anonymous.field : I
IRETURN
MAXSTACK = 1
MAXLOCALS = 0
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 4 L0
BIPUSH 10
PUTSTATIC test/Anonymous.field : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
上面字节码有点长,我们主要关注以下几个点:
static INNERCLASS test/Anonymous$1 null null
上来有一段声明内部类的地方声明了有个匿名的内部类test/Anonymous$1
- 在main函数中直接调用了
test/Anonymous$1.<init>
来new了一个匿名内部类,并且注意这个匿名类的构造方法调用的时候传入了一个参数ILOAD 1
,也就是前面的局部变量i
,然后就调用run方法了。 - 合成方法
access$000
把当前类的field
这个静态字段返回。
接下来我们来看最重要的匿名类的class
文件的内容,在构造方法中接受一个int
也就是上文中的i
,然后把i
set到val$i
这个字段中了。
最后在run
方法中我们看到,会将val$i
和access$000()
相加。
我们把上面字节码重新调整一下,大概就是这样,这样是不是就更容易理解内部类的工作方式了呢,我们需要注意3点关于变量捕捉。
- 1 外部类的局部变量是通过构造方法传入的,多个局部变量,构造方法入参就会有多个,没有捕捉局部变量就是空参数构造方法。
- 2 如果匿名类捕捉的是一个对象的属性,如
obj.field
,传入的是obj
这个对象。 - 3 如果匿名类捕捉的是一个其他类的
public
静态属性,如Claz.staticField
,可以直接在内部类代码中访问,所以不需要额外传入。 - 4 如果匿名类捕捉的是一个外部类
private
静态属性,如上面例子,是通过合成方法传入的,例如access$000
。
public class Anonymous {
private static int field = 10;
public static void main(String[] args) {
int i = 1;
Runnable r = new Anonymous$1(i);
r.run();
}
static access$000(){
return field;
}
}
class Anonymous$1 implements Runnable {
Anonymous$1(int i) {
this.val$i = i;
}
@Override
public void run() {
System.out.println(this.val$i + Anonymous.access$000());
}
}
匿名类中传入的变量,例如上面的i
必须是final
即不能再被修改的,原因就是防止产生歧义。
lambda
我们用一段类似的代码来对比分析lambda
public class LambdaTest {
private static int field = 10;
public static void main(String[] args) {
int i = 1;
Runnable r = () -> System.out.println(i + field);
r.run();
}
}
对应的字节码如下,
// class version 52.0 (52)
// access flags 0x21
public class test/LambdaTest {
// compiled from: LambdaTest.java
// access flags 0x19
public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
// access flags 0xA
private static I field
// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Ltest/LambdaTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 8 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 9 L1
ILOAD 1
INVOKEDYNAMIC run(I)Ljava/lang/Runnable; [
// handle kind 0x6 : 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;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
test/LambdaTest.lambda$main$0(I)V,
()V
]
ASTORE 2
L2
LINENUMBER 10 L2
ALOAD 2
INVOKEINTERFACE java/lang/Runnable.run ()V (itf)
L3
LINENUMBER 11 L3
RETURN
L4
LOCALVARIABLE args [Ljava/lang/String; L0 L4 0
LOCALVARIABLE i I L1 L4 1
LOCALVARIABLE r Ljava/lang/Runnable; L2 L4 2
MAXSTACK = 1
MAXLOCALS = 3
// access flags 0x100A
private static synthetic lambda$main$0(I)V
L0
LINENUMBER 9 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ILOAD 0
GETSTATIC test/LambdaTest.field : I
IADD
INVOKEVIRTUAL java/io/PrintStream.println (I)V
RETURN
L1
LOCALVARIABLE i I L0 L1 0
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 6 L0
BIPUSH 10
PUTSTATIC test/LambdaTest.field : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
同样有点长,我们只关注部分重点
- 1 同样有个内部类的声明
static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
,但是类型是MethodHandles$Lookup
- 2
INVOKEDYNAMIC
这一部分,他有很多行,run(I)Ljava/lang/Runnable;
指要实现的方法名是run
,调用点的签名是入参int
返回一个Runnable
类型,这里简单理解调用点就是产生我们想要的Runnable
匿名类的一个入口点。 - 3 接下来
java/lang/invoke/LambdaMetafactory.metafactory...
这一段非常长,其实就是调用LambdaMetafactory
类的metafactory
方法,这个方法有6个入参Lookup
当前调用上下文,由jvm
自行填充;String
是方法名,这里就是run
;第一个MethodType
是接入点的方法签名,即入参int返回Runnable;第二个MethodType
是接口中方法签名,这里就是run的()void
;MethodHandle
则是真正承载接口实现的一个方法,这个函数是编译器合成的方法,这里是下面的lambda$main$0
方法;第三个MethodType
是要实现的方法的签名,这里与第二个MethodType
一致都是()void
。最后返回值是CallSite
类型,一个调用的接入点,这个对象调用getTarget().invoke(xx)
就可以返回一个目标类型,即Runnable
。 - 4 再往后
()V, test/LambdaTest.lambda$main$0(I)V, ()V
这三个对应的就是上面提到的方法入参,前面3个入参由jvm填充,这是对应后面3个入参。 - 5
lambda$main$0(I)V
,这个private static
的合成方法就是真正实现接口的函数,注意这里有个int
的入参,引入从上下文中捕捉了变量int。
上面简单的lambda
去糖后,与下面的java代码是等价的。
import java.lang.invoke.*;
public class LambdaTest {
private static int field = 10;
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
// 调用点,传入int返回一个Runnable
MethodType invokedType = MethodType.methodType(Runnable.class, int.class);
// 要实现的run方法签名,无入参无返回值
MethodType interfaceMethodType = MethodType.methodType(void.class);
MethodHandle targetMethod = lookup.findStatic(LambdaTest.class, "lambda$main$0", MethodType.methodType(void.class, int.class));
CallSite site = LambdaMetafactory.metafactory(lookup, "run",
invokedType, interfaceMethodType, targetMethod, interfaceMethodType
);
MethodHandle factory = site.getTarget();
int i = 1;
Runnable r = (Runnable) factory.invoke(i);
r.run();
}
private static void lambda$main$0(int i) {
System.out.println(i + field);
}
}
以上就有Runnbale
的lambda,并且有变量捕捉的场景去糖后的代码,也是可以java直接运行的,此外为了加深理解,可以尝试把Runnable
替换成Function
接口的R apply(T t)
方法,这个有入参有返回值,会是更复杂的场景。这里不再赘述了。
同样的lambda
中传入的变量也需要是final
的不能再被修改的,也是为了防止歧义。与匿名类部分一样,lambda
如果捕捉的是obj.field
,实际传入的是obj
对象,而不是直接传入filed
;如果捕捉的是当前类的静态属性,则直接可以在合成方法中获取,因为都是当前类的静态属性,就不需要像匿名类那样有额外的access$000
方法。