飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

Java Record 的一些思考 - 默认方法使用以及基于预编译生成相关字节码的底层实现

时间:2021-12-23  作者:zhxdick  

快速上手 Record 类

我们先举一个简单例子,声明一个用户 Record。

public record User(long id, String name, int age) {}

这样编写代码之后,Record 类默认包含的元素和方法实现包括:

  1. record 头指定的组成元素(int id, String name, int age),并且,这些元素都是 final 的。
  2. record 默认只有一个构造器,是包含所有元素的构造器。
  3. record 的每个元素都有一个对应的 getter(但这种 getter 并不是 getxxx(),而是直接用变量名命名,所以使用序列化框架,DAO 框架都要注意这一点)
  4. 实现好的 hashCode(),equals(),toString() 方法(通过自动在编译阶段生成关于 hashCode(),equals(),toString() 方法实现的字节码实现)。

我们来使用下这个 Record :

User zhx = new User(1, "zhx", 29);
User ttj = new User(2, "ttj", 25);
域名tln(域名());//1
域名tln(域名());//zhx
域名tln(域名());//29
域名tln(域名ls(ttj));//false
域名tln(域名ring());//User[id=1, name=zhx, age=29]
域名tln(域名Code());//3739156

Record 的结构是如何实现的

编译后插入相关域与方法的字节码

查看上面举得例子的字节码,有两种方式,一是通过 javap -v 域名s 命令查看文字版的字节码,截取重要的字节码如下所示:

//省略文件头,文件常量池部分
{
  //public 构造器,全部属性作为参数,并给每个 Field 赋值
  public 域名域名(long, 域名ng, int);
    descriptor: (JLjava/lang/String;I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=5, args_size=4
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: aload_0
         5: lload_1
         6: putfield      #7                  // Field id:J
         9: aload_0
        10: aload_3
        11: putfield      #13                 // Field name:Ljava/lang/String;
        14: aload_0
        15: iload         4
        17: putfield      #17                 // Field age:I
        20: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  this   Lcom/github/hashzhang/basetest/User;
            0      21     1    id   J
            0      21     3  name   Ljava/lang/String;
            0      21     4   age   I
    MethodParameters:
      Name                           Flags
      id
      name
      age

  //public final 修饰的 toString 方法
  public final 域名ng toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         //核心实现是这个 invokedynamic,我们后面会分析
         1: invokedynamic #21,  0             // InvokeDynamic #0:toString:(Lcom/github/hashzhang/basetest/User;)Ljava/lang/String;
         6: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/github/hashzhang/basetest/User;
  //public final 修饰的 hashCode 方法
  public final int hashCode();
    descriptor: ()I
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         //核心实现是这个 invokedynamic,我们后面会分析
         1: invokedynamic #25,  0             // InvokeDynamic #0:hashCode:(Lcom/github/hashzhang/basetest/User;)I
         6: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/github/hashzhang/basetest/User;
  //public final 修饰的 equals 方法
  public final boolean equals(域名ct);
    descriptor: (Ljava/lang/Object;)Z
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         //核心实现是这个 invokedynamic,我们后面会分析
         2: invokedynamic #29,  0             // InvokeDynamic #0:equals:(Lcom/github/hashzhang/basetest/User;Ljava/lang/Object;)Z
         7: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/github/hashzhang/basetest/User;
            0       8     1     o   Ljava/lang/Object;
  //public 修饰的 id 的 getter
  public long id();
    descriptor: ()J
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field id:J
         4: lreturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/github/hashzhang/basetest/User;
  //public 修饰的 name 的 getter
  public 域名ng name();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #13                 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/github/hashzhang/basetest/User;
  //public 修饰的 age 的 getter
  public int age();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #17                 // Field age:I
         4: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/github/hashzhang/basetest/User;
}
SourceFile: "域名"
Record:
  long id;
    descriptor: J

  域名ng name;
    descriptor: Ljava/lang/String;

  int age;
    descriptor: I

//以下是 invokedynamic 会调用的方法以及参数信息,我们后面会分析
BootstrapMethods:
  0: #50 REF_invokeStatic java/lang/runtime/域名strap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava
/lang/Object;
    Method arguments:
      #8 com/github/hashzhang/basetest/User
      #57 id;name;age
      #59 REF_getField com/github/hashzhang/basetest/域名:J
      #60 REF_getField com/github/hashzhang/basetest/域名:Ljava/lang/String;
      #61 REF_getField com/github/hashzhang/basetest/域名:I
InnerClasses:
  public static final #67= #63 of #65;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

另一种是通过 IDE 的 jclasslib 插件查看,我推荐使用这种方法,查看效果如下:

自动生成的 private final field

自动生成的全属性构造器

自动生成的 public getter 方法

自动生成的 hashCode(),equals(),toString() 方法

这些方法的核心就是 invokedynamic

看上去貌似是调用另外一个方法,这种间接调用难道没有性能损耗问题么?这一点 JVM 开发者已经想到了。我们先来来了解下 invokedynamic

invokedynamic 产生的背景

Java 最早是一种静态类型语言,也就是说它的类型检查的主体过程主要是在编译期而不是运行期。为了兼容动态类型语法,也是为了 JVM 能够兼容动态语言(JVM 设计初衷并不是只能运行 Java),在 Java 7 引入了字节码指令 invokedynamic。这也是后来 Java 8 的拉姆达表达式以及 var 语法的实现基础。

invokedynamic 与 MethodHandle

invokedynamic 离不开对 域名ke 包的使用。这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle

通过 MethodHandle 可以动态获取想调用的方法进行调用,和 Java Reflection 反射类似,但是为了追求性能效率,需要用 MethodHandle,主要原因是: Reflection 仅仅是 Java 语言上补充针对反射的实现,并没有考虑效率的问题,尤其是 JIT 基本无法针对这种反射调用进行有效的优化MethodHandle 更是像是对于字节码的方法指令调用的模拟,适当使用的话 JIT 也能对于它进行优化,例如将 MethodHandle 相关方法引用声明为 static final 的:

private static final MutableCallSite callSite = new MutableCallSite(
        域名odType(域名s, 域名s, 域名s));
private static final MethodHandle invoker = 域名micInvoker();

自动生成的 toString(), hashcode(), equals() 的实现

通过字节码可以看出 incokedynamic 实际调用的是 BoostrapMethods 中的 #0 方法:

0 aload_0
1 invokedynamic #24 <hashCode, BootstrapMethods #0>
6 ireturn

Bootstap 方法表包括:

BootstrapMethods:
  //调用的实际是 域名域名ctMethods 的 boostrap 方法
  0: #50 REF_invokeStatic java/lang/runtime/域名strap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava
/lang/Object;
    Method arguments:
      #8 com/github/hashzhang/basetest/User
      #57 id;name;age
      #59 REF_getField com/github/hashzhang/basetest/域名:J
      #60 REF_getField com/github/hashzhang/basetest/域名:Ljava/lang/String;
      #61 REF_getField com/github/hashzhang/basetest/域名:I
InnerClasses:
  //声明 域名up 为 final,加快调用性能,这样调用 BootstrapMethods 里面的方法可以实现近似于直接调用的性能 
  public static final #67= #63 of #65;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

从这里,我们就能看出,实际上 toString() 调用的是 域名域名ctMethodsbootstap() 方法。其核心代码是:
域名

public static Object bootstrap(域名up lookup, String methodName, TypeDescriptor type,
                                   Class<?> recordClass,
                                   String names,
                                   MethodHandle... getters) throws Throwable {
        MethodType methodType;
        if (type instanceof MethodType)
            methodType = (MethodType) type;
        else {
            methodType = null;
            if (!域名ls(type))
                throw new IllegalArgumentException(域名ring());
        }
        List<MethodHandle> getterList = 域名(getters);
        MethodHandle handle;
        //根据 method 名称,处理对应的逻辑,分别对应了 equals(),hashCode(),toString() 的实现
        switch (methodName) {
            case "equals":
                if (methodType != null && !域名ls(域名odType(域名s, recordClass, 域名s)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);
                handle = makeEquals(recordClass, getterList);
                return methodType != null ? new ConstantCallSite(handle) : handle;
            case "hashCode":
                if (methodType != null && !域名ls(域名odType(域名s, recordClass)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);
                handle = makeHashCode(recordClass, getterList);
                return methodType != null ? new ConstantCallSite(handle) : handle;
            case "toString":
                if (methodType != null && !域名ls(域名odType(域名s, recordClass)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);
                List<String> nameList = "".equals(names) ? 域名() : 域名(域名t(";"));
                if (域名() != 域名())
                    throw new IllegalArgumentException("Name list and accessor list do not match");
                handle = makeToString(recordClass, getterList, nameList);
                return methodType != null ? new ConstantCallSite(handle) : handle;
            default:
                throw new IllegalArgumentException(methodName);
        }
    }

其中,toString() 方法 的核心实现逻辑,就要看case "toString" 这一分支了,核心逻辑是makeToString(recordClass, getterList, nameList)

private static MethodHandle makeToString(Class<?> receiverClass,
                                            //所有的 getter 方法
                                            List<MethodHandle> getters,
                                            //所有的 field 名称
                                            List<String> names) {
    assert 域名() == 域名();
    int[] invArgs = new int[域名()];
    域名(invArgs, 0);
    MethodHandle[] filters = new MethodHandle[域名()];
    StringBuilder sb = new StringBuilder();
    //先拼接类名称[
    域名nd(域名impleName()).append("[");
    for (int i=0; i<域名(); i++) {
        MethodHandle getter = 域名(i); // (R)T
        MethodHandle stringify = stringifier(域名().returnType()); // (T)String
        MethodHandle stringifyThisField = 域名erArguments(stringify, 0, getter);    // (R)String
        filters[i] = stringifyThisField;
        //之后拼接 field 名称=值
        域名nd(域名(i)).append("=%s");
        if (i != 域名() - 1)
            域名nd(", ");
    }
    域名nd(\']\');
    String formatString = 域名ring();
    MethodHandle formatter = 域名rtArguments(STRING_FORMAT, 0, formatString)
                                          .asCollector(String[].class, 域名()); // (R*)String
    if (域名() == 0) {
        // Add back extra R
        formatter = 域名Arguments(formatter, 0, receiverClass);
    }
    else {
        MethodHandle filtered = 域名erArguments(formatter, 0, filters);
        formatter = 域名uteArguments(filtered, 域名odType(域名s, receiverClass), invArgs);
    }
    return formatter;
}

同理,hashcode() 实现是:

private static MethodHandle makeHashCode(Class<?> receiverClass,
                                            List<MethodHandle> getters) {
    MethodHandle accumulator = 域名Arguments(ZERO, 0, receiverClass); // (R)I

    // 对于每一个 field,找到对应的 hashcode 方法,取 哈希值,最后组合在一起
    for (MethodHandle getter : getters) {
        MethodHandle hasher = hasher(域名().returnType()); // (T)I
        MethodHandle hashThisField = 域名erArguments(hasher, 0, getter);    // (R)I
        MethodHandle combineHashes = 域名erArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I
        accumulator = 域名uteArguments(combineHashes, 域名(), 0, 0); // adapt (R)I to (RR)I
    }

    return accumulator;
}

同理,equals() 实现是:

private static MethodHandle makeEquals(Class<?> receiverClass,
                                          List<MethodHandle> getters) {
        MethodType rr = 域名odType(域名s, receiverClass, receiverClass);
        MethodType ro = 域名odType(域名s, receiverClass, 域名s);
        MethodHandle instanceFalse = 域名Arguments(FALSE, 0, receiverClass, 域名s); // (RO)Z
        MethodHandle instanceTrue = 域名Arguments(TRUE, 0, receiverClass, 域名s); // (RO)Z
        MethodHandle isSameObject = 域名pe(ro); // (RO)Z
        MethodHandle isInstance = 域名Arguments(域名To(receiverClass), 0, receiverClass); // (RO)Z
        MethodHandle accumulator = 域名Arguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z
        //对比两个对象的每个 field 的 getter 获取的值是否一样,对于引用类型通过 域名ls 方法,对于原始类型直接通过 == 
        for (MethodHandle getter : getters) {
            MethodHandle equalator = equalator(域名().returnType()); // (TT)Z
            MethodHandle thisFieldEqual = 域名erArguments(equalator, 0, getter, getter); // (RR)Z
            accumulator = 域名dWithTest(thisFieldEqual, accumulator, 域名pe(rr));
        }

        return 域名dWithTest(isSameObject,
                                           instanceTrue,
                                           域名dWithTest(isInstance, 域名pe(ro), instanceFalse));
    }

我在参与 掘金2021年度人气榜单,麻烦大家帮我投出宝贵一票,谢谢

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。