Lombok @Data注解趟坑计
1、背景描述
开发过程中大家创建对象或者通过MyBatis插件自动反转生成实体类的时候,可能经常会使用Lombok的注解,Lombok是一个很优秀的Java库,简单的几个注解,可以干掉一大片的模版代码,比如@Data注解,可以减少很多get set方法,使代码看起来更加优雅,但是这个注解在有些情况下会有坑,我们在使用过程中还是要慎用,接下来,我会通过一个例子来复现这个坑
前段时间在做一个需求,背景是一个包裹装了多个商品的情况,测试反馈在出库的时候报错了,第一反应就是去看出库相关的代码,发现都是一年前的代码,然后就去查数据库,发现包裹关联的数据没有对应的出库单明细,想来肯定是生成包裹的时候,更新的数据不对,然后去看拣货下架的代码,发现也都是一年前的代码,感觉十分诡异,然后在测试环境加了很多日志,抓取日志报文后,一通debug,最后发现是Lombok @Data注解的坑
2、这个坑是什么呢?
子类对象如果只添加了@Data注解的时候,会自动重写hashCode() equals() toString()方法方法,且这些方法不关注父对象属性,多个子类对象,如果只有父类属性有值,计算出来的hash值会相同,如果有使用hashCode() equals()的地方,可能会有问题。
3、问题复现
创建父类Person
@Data
public class Person {
private Integer id;
private String name;
private String sex;
}
创建子类Student,手写get set方法
public class Student extends Person {
private String school;
private Integer grade;
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public Integer getGrade() {
return grade;
}
public void setGrade(Integer grade) {
this.grade = grade;
}
}
测试Demo BeanDataDemo
@Slf4j
public class BeanDataDemo {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Student student = new Student();
student.setId(i);
student.setName("小明" + i);
student.setSex(i % 2 == 0 ? "男" : "女");
studentList.add(student);
}
log.info("studentList {}", JSONUtil.toJsonStr(studentList));
List<Student> distinctStudentList = studentList.stream().distinct().collect(Collectors.toList());
log.info("distinctStudentList {}", JSONUtil.toJsonStr(distinctStudentList));
}
}
运行结果
studentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
distinctStudentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
我们把Student的get set方法去掉,改用@Data注解
@Data
public class Student extends Person {
private String school;
private Integer grade;
}
再次运行 测试Demo BeanDataDemo 得到运行结果
studentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
distinctStudentList [{"sex":"男","name":"小明0","id":0}]
通过日志对比,我们发现,手写get set方法的情况下,studentList和distinctStudentList经过stream().distinct()去重前后都是一致的,但加了@Data注解之后,studentList经过stream().distinct()去重之后,distinctStudentList只剩一个对象了,但在stream().distinct()之前,studentList对象的属性是不同的呀,这到底是为什么呢?
我们知道,stream().distinct()去重原理是靠hashcode()方法定位槽,equals()方法判断是否是同一个对象,如果是则排重被去掉,不是的话保留,通过debug,我们发现,虽然student的父类属性不同,但多个对象的hashCode确是相同的,所以distinct之后就只剩一个对象了

那么问题来了,为什么属性不同的对象,hashCode确是相同呢?手写get set方法的情况下为什么没问题呢?于是我们看了编译后的.class文件
有@Data注解的 Student.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.xm.study.demo.utils;
public class Student extends Person {
private String school;
private Integer grade;
public Student() {
}
public String getSchool() {
return this.school;
}
public Integer getGrade() {
return this.grade;
}
public void setSchool(final String school) {
this.school = school;
}
public void setGrade(final Integer grade) {
this.grade = grade;
}
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof Student)) {
return false;
} else {
Student other = (Student)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$grade = this.getGrade();
Object other$grade = other.getGrade();
if (this$grade == null) {
if (other$grade != null) {
return false;
}
} else if (!this$grade.equals(other$grade)) {
return false;
}
Object this$school = this.getSchool();
Object other$school = other.getSchool();
if (this$school == null) {
if (other$school != null) {
return false;
}
} else if (!this$school.equals(other$school)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof Student;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $grade = this.getGrade();
int result = result * 59 + ($grade == null ? 43 : $grade.hashCode());
Object $school = this.getSchool();
result = result * 59 + ($school == null ? 43 : $school.hashCode());
return result;
}
public String toString() {
return "Student(school=" + this.getSchool() + ", grade=" + this.getGrade() + ")";
}
}
手写get set方法的 Student.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.xm.study.demo.utils;
public class Student extends Person {
private String school;
private Integer grade;
public Student() {
}
public String getSchool() {
return this.school;
}
public void setSchool(String school) {
this.school = school;
}
public Integer getGrade() {
return this.grade;
}
public void setGrade(Integer grade) {
this.grade = grade;
}
}
通过对比class文件,我们发现,添加@Data注解之后,默认会重写子类的 hashCode() equals() toString()方法,而且这些方法中只关注了子类的属性,没有父类的属性,我们在测试Demo中只给Student的父类属性赋值了,子类属性均为默认值,这样就解释了,为什么我们在studentList中不管获取哪个对象,他的hashCode都是相同的,随后我们去查了Lombok的官方文档,发现官方文档有写,可以通过注解@EqualsAndHashCode(callSuper = false/true) 中的callSuper来指定是否关注父类属性


随后,我们对加了@Data注解的Student 类稍作改造
@Data
@EqualsAndHashCode(callSuper = true)
public class Student extends Person {
private String school;
private Integer grade;
}
然后,再次debug运行测试Demo BeanDataDemo ,并获取到运行结果

studentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
distinctStudentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
这时候我们在去看加了@Data和@EqualsAndHashCode(callSuper = true)注解的Student.class,会发现equals()方法中多了一行关于父类的equals()判断 !super.equals(o) ,hashCode()方法中多了一行关于父类的hashCode() super.hashCode()
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.xm.study.demo.utils;
public class Student extends Person {
private String school;
private Integer grade;
public Student() {
}
public String getSchool() {
return this.school;
}
public Integer getGrade() {
return this.grade;
}
public void setSchool(final String school) {
this.school = school;
}
public void setGrade(final Integer grade) {
this.grade = grade;
}
public String toString() {
return "Student(school=" + this.getSchool() + ", grade=" + this.getGrade() + ")";
}
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof Student)) {
return false;
} else {
Student other = (Student)o;
if (!other.canEqual(this)) {
return false;
} else if (!super.equals(o)) {
return false;
} else {
Object this$grade = this.getGrade();
Object other$grade = other.getGrade();
if (this$grade == null) {
if (other$grade != null) {
return false;
}
} else if (!this$grade.equals(other$grade)) {
return false;
}
Object this$school = this.getSchool();
Object other$school = other.getSchool();
if (this$school == null) {
if (other$school != null) {
return false;
}
} else if (!this$school.equals(other$school)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof Student;
}
public int hashCode() {
int PRIME = true;
int result = super.hashCode();
Object $grade = this.getGrade();
result = result * 59 + ($grade == null ? 43 : $grade.hashCode());
Object $school = this.getSchool();
result = result * 59 + ($school == null ? 43 : $school.hashCode());
return result;
}
}
至此,问题分析完毕并解决,针对这种父子关系的实体类,我们的解决方式有两种
①、不使用@Data注解,手写get set方法,根据需要看是否需要重写hashCode和equals方法
②、同时使用@Data注解和@EqualsAndHashCode(callSuper = true)