场景
Springboot项目中使用Lombok,实体采用@Data注解。运行过程中报Caused by: java.lang.StackOverflowError。
@Data到底做了啥?
1、帮助我们生成Get/Set方法,简化javabean的代码冗余
2、帮助我们重写equals方法,
3、帮助我们重写hashCode
4、大大提高了JavaBean的执行效率(?)
StackOverflowError是哪里抛出的异常?
先来看StackOverflowError和OutOfMemoryError。
在《Java虚拟机规范》中描述了这两种异常:
1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError。
2)如果虚拟机的栈内存允许动态扩展,当扩展容量无法申请到足够的内存时,将抛出OutOfMemoryError。
也就是说,由于JVM规定了栈的最大深度,因无法容纳新的栈帧而抛出StackOverflowError异常;这种情况通常预示着代码可能有出现死循环等问题。
通过查看执行log,发现TreeDTO.hashCode()方法循环抛出异常,也即出现了死循环。
@Data
public class TreeDTO implements Serializable {
private Integer id;
private String name;
private Integer pid;
private String checked;
private List<TreeDTO> children;
}
由于该类使用了@Data注解,所以该hashCode()方法由注解自动生成,所以将范围缩小至@Data上,而且这里出现了集合间包含自身的递归引用。
什么是hashCode?
equals():是用来判断两个对象是否相同,在Object类中是通过判断对象间的内存地址来决定是否相同。
hashCode():是获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。如果两个对象equals()方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个int结果。
为什么会出现该异常?
@Data注解编译后的Entity:
public class TreeDTO implements Serializable {
private Integer id;
private String name;
private Integer pid;
private String checked;
private List<TreeDTO> children;
public TreeDTO() {
}
public Integer getId() {
return this.id;
}
public String getName() {
return this.name;
}
public Integer getPid() {
return this.pid;
}
public String getChecked() {
return this.checked;
}
public List<TreeDTO> getChildren() {
return this.children;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setPid(Integer pid) {
this.pid = pid;
}
public void setChecked(String checked) {
this.checked = checked;
}
public void setChildren(List<TreeDTO> children) {
this.children = children;
}
public boolean equals(Object o) {
if(o == this) {
return true;
} else if(!(o instanceof TreeDTO)) {
return false;
} else {
TreeDTO other = (TreeDTO)o;
if(!other.canEqual(this)) {
return false;
} else {
label71: {
Object this$id = this.getId();
Object other$id = other.getId();
if(this$id == null) {
if(other$id == null) {
break label71;
}
} else if(this$id.equals(other$id)) {
break label71;
}
return false;
}
Object this$name = this.getName();
Object other$name = other.getName();
if(this$name == null) {
if(other$name != null) {
return false;
}
} else if(!this$name.equals(other$name)) {
return false;
}
label57: {
Object this$pid = this.getPid();
Object other$pid = other.getPid();
if(this$pid == null) {
if(other$pid == null) {
break label57;
}
} else if(this$pid.equals(other$pid)) {
break label57;
}
return false;
}
Object this$checked = this.getChecked();
Object other$checked = other.getChecked();
if(this$checked == null) {
if(other$checked != null) {
return false;
}
} else if(!this$checked.equals(other$checked)) {
return false;
}
Object this$children = this.getChildren();
Object other$children = other.getChildren();
if(this$children == null) {
if(other$children == null) {
return true;
}
} else if(this$children.equals(other$children)) {
return true;
}
return false;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof TreeDTO;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $id = this.getId();
int result = result * 59 + ($id == null?43:$id.hashCode());
Object $name = this.getName();
result = result * 59 + ($name == null?43:$name.hashCode());
Object $pid = this.getPid();
result = result * 59 + ($pid == null?43:$pid.hashCode());
Object $checked = this.getChecked();
result = result * 59 + ($checked == null?43:$checked.hashCode());
Object $children = this.getChildren();
result = result * 59 + ($children == null?43:$children.hashCode());
return result;
}
public String toString() {
return "TreeDTO(id=" + this.getId() + ", name=" + this.getName() + ", pid=" + this.getPid() + ", checked=" + this.getChecked() + ", children=" + this.getChildren() + ")";
}
}
由于TreeDTO中包含有自身对象的集合List,对于AbstractList的hashCode其实是把每一个子元素的hashCode经过迭代计算得到的,也就是说,要计算AbstractList的hashCode,就要把每一个子元素的hashCode先计算一遍,如果这些子元素中的某一个或子元素的子元素引用到上级对象,那么hashCode方法就会出现无限递归调用,最终出现StackOverflowError错误。
不仅仅是List集合,Set、Map、Stack也有同样的问题。
如何解决?
1、尽量不要出现集合间的递归引用。
2、使用@Getter、@Setter来替代@Data
3、@Data配合@EqualsAndHashCode(callSuper=true)一起使用,让其生成的方法中调用父类的方法。注:使用EqualsAndHashCode时,实体类必须要有继承父类,因为设置true默认是要调用父类的方法,如果没有继承,则无法使用@EqualsAndHashCode(callSuper=true),默认callSuper=false.
虽然出现这种问题的概率比较小,线上项目也是正常运行一段时间后才出现。这里不知道较高版本的JDK或者较高版本的Lombok会不会修复次问题。这里使用的是JDK-1.8以及Lombok-1.16.10。