今天写研究一个问题时,涉及到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相同:

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

经过上面的现象之后,我就有了疑问,在这之前我对@Data的了解仅仅局限于它简化了代码,依赖是lombok,包含了getter、setter方法等范围,对于其深层次的原理,我并不是很了解。因此趁着解决这个疑问的契机,研究一下@Data。
一、@Data的作用
@Data是lombok的组件,依赖是:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
相当于@Getter @Setter @NoArgsConstructor@ToString @EqualsAndHashCode这5个注解的合集。
(1)@Getter:实体类中的getXX()方法
(2)@Setter:实体类中的setXX()方法
(3)@NoArgsConstructor:无参构造方法。
(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不一样。
对比添加@Data和添加@Getter@Setter的注解的class文件可知。@Data相较于@Getter@Setter注解来说多了一些方法,其中就包含覆写hashCode 的方法。先看下普通的计算hashCode的方法
首先,之前有一个错误的认识,以为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方法
......
public int hashCode() {
int PRIME = 59;
result = 1;
Object $username = getUsername();
result = result * 59 + (($username == null) ? 43 : $username.hashCode());
Object $password = getPassword();
result = result * 59 + (($password == null) ? 43 : $password.hashCode());
Object $id = getId();
result = result * 59 + (($id == null) ? 43 : $id.hashCode());
Object $name = getName();
result = result * 59 + (($name == null) ? 43 : $name.hashCode());
Object $desc = getDesc();
result = result * 59 + (($desc == null) ? 43 : $desc.hashCode());
Object $age = getAge();
result = result * 59 + (($age == null) ? 43 : $age.hashCode());
Object $count = getCount();
result = result * 59 + (($count == null) ? 43 : $count.hashCode());
Object $value = getValue();
result = result * 59 + (($value == null) ? 43 : $value.hashCode());
Object<String> $a = (Object<String>)getA();
return result * 59 + (($a == null) ? 43 : $a.hashCode());
}
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值结果带-号,

原因是值太大,已经超出了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
然后,针对标题中的疑问,已经解开。:)