@Data注解导致的StackOverflowError

简介: Springboot项目中使用Lombok,实体采用@Data注解。运行过程中报Caused by: java.lang.StackOverflowError。

场景

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。

相关文章
|
6天前
|
Java UED
Java中的异常处理:捕获、声明与抛出
Java中的异常处理:捕获、声明与抛出
65 0
|
8月前
|
Java
JavaSE 异常之自定义异常
JavaSE 异常之自定义异常
35 0
Mockito框架抛出NullPointerException
一文详细讲解Mockito框架是怎么抛出NullPointerException的整个过程和解决方式。
5515 0
|
6天前
|
Java 程序员 编译器
JavaSE学习值之--认识异常(下)
JavaSE学习值之--认识异常(下)
33 0
|
6天前
|
Java 编译器 程序员
JavaSE学习值之--认识异常(上)
JavaSE学习值之--认识异常
41 0
|
9月前
|
Java 容器
main方法里使用@Autowired注解报空指针错误
main方法里使用@Autowired注解报空指针错误
143 0
|
11月前
|
Java
Java中异常的抛出方式
Java中异常的抛出方式
368 0
|
Java 程序员 数据库
Java问题解决录: 运行时抛出NoSuchMethodError / NoSuchFieldError异常
Java问题解决录: 运行时抛出NoSuchMethodError / NoSuchFieldError异常
Java问题解决录: 运行时抛出NoSuchMethodError / NoSuchFieldError异常
|
Java 编译器
Java中的异常(抛出异常、自定义异常等)
Java中的异常(抛出异常、自定义异常等)
227 1