Java Stream 与 HashCode 配合引起的问题

doMore 194 2023-12-23

本文所呈现的问题,是实际编码中出现的,只是删除和精简了一部分,但可以呈现出问题。
参考文章: https://yhsblog.cn/archives/ji-yi-ci-stream-bao-cuo-dao-zhi-de-dui-zhan-yi-chu

问题

在执行以下代码时,抛出 java.lang.StackOverflowError 异常。


Map<Long, Map<String, Post>> pageItemMap = pages.stream().collect(Collectors.toMap(e -> e.getDeptId(),  
        e -> e.getPostList().stream().collect(Collectors.toMap(t -> t.getPostName(), Function.identity(), (v1, v2) -> v2))));

排查

  1. 查看日志信息
    异常日志如下:
Exception in thread "main" java.lang.StackOverflowError
	at com.StreamStackTest2$Dept.hashCode(StreamStackTest2.java:58)
	at com.StreamStackTest2$Post.hashCode(StreamStackTest2.java:74)
	at java.util.AbstractList.hashCode(AbstractList.java:541)
	at com.StreamStackTest2$Dept.hashCode(StreamStackTest2.java:58)
	at com.StreamStackTest2$Post.hashCode(StreamStackTest2.java:74)
	at java.util.AbstractList.hashCode(AbstractList.java:541)
	at com.StreamStackTest2$Dept.hashCode(StreamStackTest2.java:58)
	at com.StreamStackTest2$Post.hashCode(StreamStackTest2.java:74)
	at java.util.AbstractList.hashCode(AbstractList.java:541)
	at com.StreamStackTest2$Dept.hashCode(StreamStackTest2.java:58)
	at com.StreamStackTest2$Post.hashCode(StreamStackTest2.java:74)

  1. 报错提示是由于调用 hashCode 方法引起的,但是从上面代码中,并不存在调用实体 hashcode 方法的场景。在 hashCode 方法中打断点(需要去除注解,自己手动重写)。

streaStackOverFlow

  1. 比较清晰的看到是 Collectors.toMap() 方法的 merge 时出现了异常
  2. 因为没有指定 merge 时,执行的方法,所以会走默认的。默认抛出异常时会组装输出信息,即会调用 u 的 toString() 方法。
// u -> map merge 时的 oldValue
// v -> map merge 时的 newValue 
private static <T> BinaryOperator<T> throwingMerger() {  
    return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };  
}
  1. lombok 的 注解 @ToString 的 属性 callSuper=true ,在编译的时候生成的 toString() 方法如下:
// 在 输出的时候 会 首先调用 super.toString()
public String toString() {  
    return "StreamStackTest2.Dept(super=" + super.toString() + ", deptId=" + this.getDeptId() + ", deptName=" + this.getDeptName() + ", postList=" + this.getPostList() + ")";  
}


//  callSuper=false  的情况
public String toString() {  
    return "StreamStackTest2.Dept(deptId=" + this.getDeptId() + ", deptName=" + this.getDeptName() + ", postList=" + this.getPostList() + ")";  
}

  1. 最终会走到 Object.toString()。这里会进入 hashCode 。hashCode 方法会调用所有属性的hashCode 方法,那么由于实体是相互依赖的形式,就形成了hashCode调用的死循环,从而导致了堆栈溢出。
public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}

结论

  1. 在使用 Collectors.toMap 方法时,一定要指定 merge 方法。
  2. 有相互 依赖关系的 尽量不要执行 toString 或者 hashCode 方法会造成死循环。
  3. 尽可能自己重写 toString 和 hashCode ;尽量不要指定 callSuper=true 的模式

附录

复现代码(精简版)


import lombok.Data;  
import lombok.EqualsAndHashCode;  
import lombok.ToString;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
import java.util.function.Function;  
import java.util.stream.Collectors;  

 public class StreamStackTest2 {  
  
  
    public static void main(String[] args) {  
        List<Dept> pages = new ArrayList<>();  
  
        ArrayList<Post> postList1 = new ArrayList<>();  
        Dept dept1 = new Dept(1L, "山西省防空部门", postList1);  
        ArrayList<Post> postList2 = new ArrayList<>();  
        Dept dept2 = new Dept(2L, "山西省消防部门", postList2);  
  
        Post post1 = new Post(1L, "机长", dept1);  
        Post post2 = new Post(2L, "乘务员", dept1);  
        postList1.add(post1);  
        postList1.add(post2);  
  
        Post post3 = new Post(3L, "消防员", dept2);  
        Post post4 = new Post(4L, "消防员", dept2);  
        postList2.add(post3);  
        postList2.add(post4);  
  
        pages.add(dept1);  
        pages.add(dept2);  
        System.out.println("数据组装完成");  
        Map<Long, Map<String, Post>> pageItemMap = pages.stream().collect(Collectors.toMap(e -> e.getDeptId(),  
                e -> e.getPostList().stream().collect(Collectors.toMap(t -> t.getPostName(), Function.identity()))));  
  
        System.out.println(pageItemMap);  
    }  
  
  
    private static class Parent {  
        @Override  
        public String toString() {  
            return super.toString();  
        }  
    }  
  
  
    @Data  
    @EqualsAndHashCode(callSuper = true)  
    @ToString(callSuper = true)  
    private static class Dept extends Parent {  
        private Long deptId;  
        private String deptName;  
        private List<Post> postList;  
  
        public Dept(Long deptId, String deptName, List<Post> postList) {  
            this.deptId = deptId;  
            this.deptName = deptName;  
            this.postList = postList;  
        }  
    }  
  
  
    @Data  
    @EqualsAndHashCode(callSuper = true)  
    @ToString(callSuper = true)  
    private static class Post extends Parent {  
        private Long postId;  
        private String postName;  
        private Dept dept;  
  
        public Post(Long postId, String postName, Dept dept) {  
            this.postId = postId;  
            this.postName = postName;  
            this.dept = dept;  
        }  
    }  
  
}