java的匿名类与lambda

11 min read,created at 2024-11-09

在学习java字节码的时候,会发现两个神奇的现象:

  • 1 java匿名类,会在编译期创建一个类似宿主类名$1.class这样的文件,并且如果要用instrumentation去替换原始类的时候,会发现如果新加了匿名类是不允许的。
  • 2 lambda的代码是不会在编译期生成匿名类的文件的,它使用的是invokeDynamic字节码指令。

image

匿名类比较简单,从字节码来看他就是个内部类。

image

但lambda就复杂了,在字节码中会发现3个不同,首先是多了一个private staticlambda$宿主方法名$0这样的方法,这个方法的返回值类型就是lambda返回值类型。第二就是也有一个innerClass但是这个内部类并不是宿主类名$1后面会讲。第三个就是BootstrapMethods引导方法,他的主要作用就是配合invokeDynamic来找到动态调用点。

image

匿名类

下面我们详细看一下匿名类,下面是一个会从上下文中捕捉变量的匿名类,他更具有代表性。

Anonymous.java
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,然后把iset到val$i这个字段中了。

image

image

image

最后在run方法中我们看到,会将val$iaccess$000()相加。

image

我们把上面字节码重新调整一下,大概就是这样,这样是不是就更容易理解内部类的工作方式了呢,我们需要注意3点关于变量捕捉。

  • 1 外部类的局部变量是通过构造方法传入的,多个局部变量,构造方法入参就会有多个,没有捕捉局部变量就是空参数构造方法。
  • 2 如果匿名类捕捉的是一个对象的属性,如obj.field,传入的是obj这个对象。
  • 3 如果匿名类捕捉的是一个其他类的public静态属性,如Claz.staticField,可以直接在内部类代码中访问,所以不需要额外传入。
  • 4 如果匿名类捕捉的是一个外部类private静态属性,如上面例子,是通过合成方法传入的,例如access$000
Anonymous.java
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

LambdaTest.java
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的()voidMethodHandle则是真正承载接口实现的一个方法,这个函数是编译器合成的方法,这里是下面的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代码是等价的。

LambdaTest.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方法。