Java Record 的一些思考 - 默认方法使用以及基于预编译生成相关字节码的底层实现
快速上手 Record 类
我们先举一个简单例子,声明一个用户 Record。
public record User(long id, String name, int age) {}
这样编写代码之后,Record 类默认包含的元素和方法实现包括:
- record 头指定的组成元素(
int id, String name, int age
),并且,这些元素都是 final 的。 - record 默认只有一个构造器,是包含所有元素的构造器。
- record 的每个元素都有一个对应的 getter(但这种 getter 并不是 getxxx(),而是直接用变量名命名,所以使用序列化框架,DAO 框架都要注意这一点)
- 实现好的 hashCode(),equals(),toString() 方法(通过自动在编译阶段生成关于 hashCode(),equals(),toString() 方法实现的字节码实现)。
我们来使用下这个 Record :
User zhx = new User(1, "zhx", 29);
User ttj = new User(2, "ttj", 25);
System.out.println(zhx.id());//1
System.out.println(zhx.name());//zhx
System.out.println(zhx.age());//29
System.out.println(zhx.equals(ttj));//false
System.out.println(zhx.toString());//User[id=1, name=zhx, age=29]
System.out.println(zhx.hashCode());//3739156
Record 的结构是如何实现的
编译后插入相关域与方法的字节码
查看上面举得例子的字节码,有两种方式,一是通过 javap -v User.class
命令查看文字版的字节码,截取重要的字节码如下所示:
//省略文件头,文件常量池部分
{
//public 构造器,全部属性作为参数,并给每个 Field 赋值
public com.github.hashzhang.basetest.User(long, java.lang.String, 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 java.lang.String 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(java.lang.Object);
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 java.lang.String 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: "User.java"
Record:
long id;
descriptor: J
java.lang.String name;
descriptor: Ljava/lang/String;
int age;
descriptor: I
//以下是 invokedynamic 会调用的方法以及参数信息,我们后面会分析
BootstrapMethods:
0: #50 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(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/User.id:J
#60 REF_getField com/github/hashzhang/basetest/User.name:Ljava/lang/String;
#61 REF_getField com/github/hashzhang/basetest/User.age: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 离不开对 java.lang.invoke 包的使用。这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle
。
通过 MethodHandle
可以动态获取想调用的方法进行调用,和 Java Reflection
反射类似,但是为了追求性能效率,需要用 MethodHandle
,主要原因是: Reflection
仅仅是 Java 语言上补充针对反射的实现,并没有考虑效率的问题,尤其是 JIT 基本无法针对这种反射调用进行有效的优化。MethodHandle
更是像是对于字节码的方法指令调用的模拟,适当使用的话 JIT 也能对于它进行优化,例如将 MethodHandle
相关方法引用声明为 static final 的:
private static final MutableCallSite callSite = new MutableCallSite(
MethodType.methodType(int.class, int.class, int.class));
private static final MethodHandle invoker = callSite.dynamicInvoker();
自动生成的 toString(), hashcode(), equals() 的实现
通过字节码可以看出 incokedynamic 实际调用的是 BoostrapMethods
中的 #0 方法:
0 aload_0
1 invokedynamic #24 <hashCode, BootstrapMethods #0>
6 ireturn
Bootstap 方法表包括:
BootstrapMethods:
//调用的实际是 java.lang.runtime.ObjectMethods 的 boostrap 方法
0: #50 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(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/User.id:J
#60 REF_getField com/github/hashzhang/basetest/User.name:Ljava/lang/String;
#61 REF_getField com/github/hashzhang/basetest/User.age:I
InnerClasses:
//声明 MethodHandles.Lookup 为 final,加快调用性能,这样调用 BootstrapMethods 里面的方法可以实现近似于直接调用的性能
public static final #67= #63 of #65; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
从这里,我们就能看出,实际上 toString() 调用的是 java.lang.runtime.ObjectMethods
的 bootstap()
方法。其核心代码是:
ObjectMethods.java
public static Object bootstrap(MethodHandles.Lookup 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 (!MethodHandle.class.equals(type))
throw new IllegalArgumentException(type.toString());
}
List<MethodHandle> getterList = List.of(getters);
MethodHandle handle;
//根据 method 名称,处理对应的逻辑,分别对应了 equals(),hashCode(),toString() 的实现
switch (methodName) {
case "equals":
if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
throw new IllegalArgumentException("Bad method type: " + methodType);
handle = makeEquals(recordClass, getterList);
return methodType != null ? new ConstantCallSite(handle) : handle;
case "hashCode":
if (methodType != null && !methodType.equals(MethodType.methodType(int.class, recordClass)))
throw new IllegalArgumentException("Bad method type: " + methodType);
handle = makeHashCode(recordClass, getterList);
return methodType != null ? new ConstantCallSite(handle) : handle;
case "toString":
if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass)))
throw new IllegalArgumentException("Bad method type: " + methodType);
List<String> nameList = "".equals(names) ? List.of() : List.of(names.split(";"));
if (nameList.size() != getterList.size())
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 getters.size() == names.size();
int[] invArgs = new int[getters.size()];
Arrays.fill(invArgs, 0);
MethodHandle[] filters = new MethodHandle[getters.size()];
StringBuilder sb = new StringBuilder();
//先拼接类名称[
sb.append(receiverClass.getSimpleName()).append("[");
for (int i=0; i<getters.size(); i++) {
MethodHandle getter = getters.get(i); // (R)T
MethodHandle stringify = stringifier(getter.type().returnType()); // (T)String
MethodHandle stringifyThisField = MethodHandles.filterArguments(stringify, 0, getter); // (R)String
filters[i] = stringifyThisField;
//之后拼接 field 名称=值
sb.append(names.get(i)).append("=%s");
if (i != getters.size() - 1)
sb.append(", ");
}
sb.append(']');
String formatString = sb.toString();
MethodHandle formatter = MethodHandles.insertArguments(STRING_FORMAT, 0, formatString)
.asCollector(String[].class, getters.size()); // (R*)String
if (getters.size() == 0) {
// Add back extra R
formatter = MethodHandles.dropArguments(formatter, 0, receiverClass);
}
else {
MethodHandle filtered = MethodHandles.filterArguments(formatter, 0, filters);
formatter = MethodHandles.permuteArguments(filtered, MethodType.methodType(String.class, receiverClass), invArgs);
}
return formatter;
}
同理,hashcode()
实现是:
private static MethodHandle makeHashCode(Class<?> receiverClass,
List<MethodHandle> getters) {
MethodHandle accumulator = MethodHandles.dropArguments(ZERO, 0, receiverClass); // (R)I
// 对于每一个 field,找到对应的 hashcode 方法,取 哈希值,最后组合在一起
for (MethodHandle getter : getters) {
MethodHandle hasher = hasher(getter.type().returnType()); // (T)I
MethodHandle hashThisField = MethodHandles.filterArguments(hasher, 0, getter); // (R)I
MethodHandle combineHashes = MethodHandles.filterArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I
accumulator = MethodHandles.permuteArguments(combineHashes, accumulator.type(), 0, 0); // adapt (R)I to (RR)I
}
return accumulator;
}
同理,equals()
实现是:
private static MethodHandle makeEquals(Class<?> receiverClass,
List<MethodHandle> getters) {
MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z
//对比两个对象的每个 field 的 getter 获取的值是否一样,对于引用类型通过 Objects.equals 方法,对于原始类型直接通过 ==
for (MethodHandle getter : getters) {
MethodHandle equalator = equalator(getter.type().returnType()); // (TT)Z
MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
}
return MethodHandles.guardWithTest(isSameObject,
instanceTrue,
MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
}
我在参与 掘金2021年度人气榜单,麻烦大家帮我投出宝贵一票,谢谢
Java Record 的一些思考 - 默认方法使用以及基于预编译生成相关字节码的底层实现的更多相关文章
- Java 8 接口中的默认方法与静态方法
Java 8 接口中的默认方法与静态方法 1. 接口中的默认方法 允许接口中包含具有具体实现的方法,该方法称"默认方法",默认方法使用用 default 关键字修饰. public ...
- Java Record 的一些思考 - 序列化相关
Java Record 序列化相关 Record 在设计之初,就是为了找寻一种纯表示数据的类型载体.Java 的 class 现在经过不断的迭代做功能加法,用法已经非常复杂,各种语法糖,各种多态构造器 ...
- Java 8 访问接口的默认方法
Java 8 API提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自Google Guava库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的. 一.Opti ...
- Java 8中Lambda表达式默认方法的模板方法模式,你够了解么?
为了以更简单的术语描述模板方法,考虑这个场景:假设在一个工作流系统中,为了完成任务,有4个任务必须以给定的执行顺序执行.在这4个任务中,不同工作流系统的实现可以根据自身情况自定义任务的执行内容. 模板 ...
- Java自学-接口与继承 默认方法
默认方法 步骤 1 : 什么是默认方法 默认方法是JDK8新特性,指的是接口也可以提供具体方法了,而不像以前,只能提供抽象方法 Mortal 这个接口,增加了一个默认方法 revive,这个方法有实现 ...
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language- ...
- [转]深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
以下内容转自: 作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-l ...
- Java 8 默认方法(Default Methods)
Java 8 默认方法(Default Methods) Posted by Ebn Zhang on December 20, 2015 Java 8 引入了新的语言特性——默认方法(Default ...
- 30分钟入门Java8之默认方法和静态接口方法
30分钟入门Java8之默认方法和静态接口方法 前言 上一篇文章30分钟入门Java8之lambda表达式,我们学习了lambda表达式.现在继续Java8新语言特性的学习,今天,我们要学习的是默认方 ...
随机推荐
- [Bzoj 1432] [ZJOI2009]Function(结论推导题)
我们先看一下题目: (有没有和我一样的朋友看到这道题以为是几何不可做题 这个题目真的很难理解,并且样例也给得太水了吧! 理解题目是必不可少的(这并不是你看了半小时题目的理由)--首先我们先简化题目 1 ...
- Codeforces 1375F - Integer Game(交互)
Codeforces 题面传送门 & 洛谷题面传送门 一个奇怪的做法. 首先我们猜测答案总是 First.考虑什么样的情况能够一步把对方一步干掉.方便起见我们假设 \(a<b<c\ ...
- Codeforces 718E - Matvey's Birthday(思维题)
Codeforces 题面传送门 & 洛谷题面传送门 首先注意到这个图的特殊性:我们对于所有 \(s_i=s_j\) 的 \((i,j)\) 之间都连了条边,而字符集大小顶多只有 \(8\ ...
- 【机器学习与R语言】6-线性回归
目录 1.理解回归 1)简单线性回归 2)普通最小二乘估计 3)相关系数 4)多元线性回归 2.线性回归应用示例 1)收集数据 2)探索和准备数据 3)训练数据 4)评估模型 5)提高模型性能 1.理 ...
- scrapy安装及入门使用
scrapy安装及入门使用 安装 pip3.7 install Scrapy 输入scrapy命令查看是否安装成功 J-pro:myproject will$ scrapy Scrapy 2.1.0 ...
- Yarn 公平调度器案例
目录 公平调度器案例 需求 配置多队列的公平调度器 1 修改yarn-site.xml文件,加入以下从参数 2 配置fair-scheduler.xml 3 分发配置文件重启yarn 4 测试提交任务 ...
- 🏆【Alibaba中间件技术系列】「Sentinel技术专题」分布式系统的流量防卫兵的基本介绍(入门源码介绍)
推荐资料 官方文档 官方demo Sentinel 是什么? 随着微服务的流行,服务和服务之间的稳定性变得越来越重要.Sentinel 以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度保护 ...
- 【MarkDown】--使用教程
MarkDown使用教程 目录 MarkDown使用教程 一. 常用设置 1.1 目录 1.2 标题 1.3 文本样式 (1)引用 (2)高亮 (3)强调 (4)水平线 (5)上下标 (6)插入代码 ...
- css通配样式初始化(多款,供君自选)
腾讯官网 body,ol,ul,h1,h2,h3,h4,h5,h6,p,th,td,dl,dd,form,fieldset,legend,input,textarea,select{margin:0; ...
- Java SSLSocket
Java SSLSocket JSSE(Java Security Socket Extension)是Sun公司为了解决互联网信息安全传输提出的一个解决方案,它实现了SSL和TSL协议,包含了数据加 ...