2025年3月26日 星期三 甲辰(龙)年 月廿五 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Java

@Data注解及其hashCode设值的研究

时间:02-01来源:作者:点击数:66

今天写原型模式的示例时,涉及到Object的clone方法,发现了一个问题,加了@Data的实体在使用clone方法之后,二者比对的hash值相同,使用get、set方法的实体,在使用clone方法之后,二者比对的hash值不同,把示例贴出来,如下:

1.使用@Data:

  • @Data
  • public class User implements Cloneable {
  • private String name;
  • private String sex;
  • private Integer age;
  • private School school;
  • @Override
  • protected User clone() throws CloneNotSupportedException {
  • return (User) super.clone();
  • }
  • @Data
  • public static class School {
  • private String name;
  • }
  • }

2.使用getter、setter

  • public class User implements Cloneable {
  • private String name;
  • private String sex;
  • private Integer age;
  • private School school;
  • public String getName() {
  • return name;
  • }
  • public void setName(String name) {
  • this.name = name;
  • }
  • public String getSex() {
  • return sex;
  • }
  • public void setSex(String sex) {
  • this.sex = sex;
  • }
  • public Integer getAge() {
  • return age;
  • }
  • public void setAge(Integer age) {
  • this.age = age;
  • }
  • public School getSchool() {
  • return school;
  • }
  • public void setSchool(School school) {
  • this.school = school;
  • }
  • @Override
  • protected User clone() throws CloneNotSupportedException {
  • return (User) super.clone();
  • }
  • public static class School {
  • private String name;
  • public String getName() {
  • return name;
  • }
  • public void setName(String name) {
  • this.name = name;
  • }
  • }
  • }

测试代码:

  • @Slf4j
  • public class MainTest {
  • public static void main(String[] args) throws CloneNotSupportedException {
  • User user = new User();
  • user.setName("张三");
  • user.setAge(18);
  • User.School school = new User.School();
  • school.setName("华师小学");
  • user.setSchool(school);
  • User u2 = user.clone();
  • User.School school2 = new User.School();
  • school2.setName("华师小学");
  • u2.setSchool(school2);
  • log.info(JSON.toJSONString(user));
  • log.info(JSON.toJSONString(u2));
  • log.info("user:{}", user.hashCode());
  • log.info("u2:{}", u2.hashCode());
  • log.info("{}", user.hashCode() == u2.hashCode());
  • }
  • }

使用@Data打印结果,hashCode相同:

0

使用getter、setter打印结果,hashCode不同:

0

经过上面的现象之后,我就有了疑问,在这之前我对@Data的了解仅仅局限于它简化了代码,依赖是lombok,包含了getter、setter方法等范围,对于其深层次的原理,我并不是很了解。因此趁着解决这个疑问的契机,研究一下@Data。

一、@Data的作用

@Data是lombok的组件,依赖是:

  • <dependency>
  • <groupId>org.projectlombok</groupId>
  • <artifactId>lombok</artifactId>
  • <optional>true</optional>
  • </dependency>

相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。

(1)@Getter:实体类中的getXX()方法

(2)@Setter:实体类中的setXX()方法

(3)@RequiredArgsConstructor:替代@Autowired将类注入进来。但是前提是需要将注入的类加final或者使用@notnull注解,比如:

  • @RestController("userApi")
  • @RequestMapping(value = "/api/user")
  • @Data
  • public class UserApi {
  • private final UserService userService;
  • /**
  • * 查询用户信息
  • */
  • @ApiOperation(value = "查询用户信息", httpMethod = "POST")
  • @GetMapping(path = "/test")
  • public BaseResult<User> test(@Valid @NotEmpty(message = "uid不能为空") String uid) {
  • return userService.queryUser(uid);
  • }
  • }

但是如果不加final的话,调用该接口时就会报空指针,原因是userService为空

(4)@ToString:解析当前实体类的属性,会将属性以及属性值打印出来。如:

  • User(name=张三, sex=null, age=18)

不加ToString时只会打印当前对象的class的路径以及该class对象的hashCode的16进制值,如:

  • com.my.project.designmode.prototype.User@239963d8

(5)@EqualsAndHashCode:重写当前类的equals方法以及hashCode的方法。

二、解决问题

问题点:使用@Data打印的hashCode值和使用getter、setter不一样。

首先,之前有一个错误的认识,以为hashCode值就是虚拟机的存储地址,但是实际上hashCode值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。

然后,看加了@Data的class文件如下:

  • //
  • // Source code recreated from a .class file by IntelliJ IDEA
  • // (powered by FernFlower decompiler)
  • //
  • package com.my.project.designmode.prototype;
  • public class User implements Cloneable {
  • private String name;
  • private String sex;
  • private Integer age;
  • private User.School school;
  • protected User clone() throws CloneNotSupportedException {
  • return (User)super.clone();
  • }
  • //此处是构造方法、get、set、equals、canEqual、toString方法
  • ......
  • //hashCode
  • public int hashCode() {
  • int PRIME = true;
  • int result = 1;
  • Object $age = this.getAge();
  • int result = result * 59 + ($age == null ? 43 : $age.hashCode());
  • Object $name = this.getName();
  • result = result * 59 + ($name == null ? 43 : $name.hashCode());
  • Object $sex = this.getSex();
  • result = result * 59 + ($sex == null ? 43 : $sex.hashCode());
  • Object $school = this.getSchool();
  • result = result * 59 + ($school == null ? 43 : $school.hashCode());
  • return result;
  • }
  • public static class School {
  • private String name;
  • //此处是构造方法、get、set、equals、canEqual、toString方法
  • ......
  • public int hashCode() {
  • int PRIME = true;
  • int result = 1;
  • Object $name = this.getName();
  • int result = result * 59 + ($name == null ? 43 : $name.hashCode());
  • return result;
  • }
  • }
  • }

通过上述的hashCode值可以看出来,hashCode值的计算是根据属性值的具体值计算出来的,因此如果两个类的属性、属性值完全相同,即使类名不同,那么他们的hashCode依旧是一样的。我在跟踪这个问题的时候,专门根据class文件中的hashCode的计算方式使用计算器计算了一下,结果是和打印的HashCode一样(感觉自己有点儿笨,后来才想到用程序带入值就可以计算了)。

各个值对应如下:

  • int age = Integer.valueOf(18).hashCode();
  • log.info("{}", age);//18
  • int uname = "张三".hashCode();
  • log.info("{}", uname);//774889
  • int sname = "华师小学".hashCode();
  • log.info("{}", sname);//659210033
  • int sname2 = "华师小学2".hashCode();
  • log.info("{}", sname2);//-1039325407

还有一个问题,使用@Data计算结果中,user和u2的hash值结果带-号,

0

原因是值太大,已经超出了int的范围(32位系统的范围- 2 ^ 31 ~2 ^ 31 - 1,换算成十进制为[-2147483648, 2147483647]),超出部分发生了数据溢出,二进制计算如下:

  • 计算的结果为 3372415421
  • int的最大值 2147483647
  • 已经超出,然后如何计算int溢出的结果呢?
  • 先算一下超出多少:3372415421 - 2147483647 = 1224931774,使用Integer.hexString()转换为16进制:4902f9be,之后转换为二进制:
  • 0100 1001 0000 0010 1111 1001 1011 1110
  • 和2147483647的补码相加之后为:
  • 0100 1001 0000 0010 1111 1001 1011 1110
  • +
  • 0111 1111 1111 1111 1111 1111 1111 1111
  • =
  • 1100 1001 0000 0010 1111 1001 1011 1101
  • 计算原码(原码=补码减一的绝对值取反),结果为:
  • 1100 1001 0000 0010 1111 1001 1011 1110
  • 可以看出来,最前面的符号已经变为1,溢出了。
  • 我用Integer.toBinaryString(-922551875)(负数时,此方法打印补码二进制)打印二进制结果,同样为:
  • 1100 1001 0000 0010 1111 1001 1011 1101
  • 因此验证了结果为-922551875

然后,针对标题中的疑问,已经解开。:)

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门