本文所呈现的问题,是实际编码中出现的,只是删除和精简了一部分,但可以呈现出问题。
参考文章: 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))));
排查
- 查看日志信息
异常日志如下:
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)
- 报错提示是由于调用 hashCode 方法引起的,但是从上面代码中,并不存在调用实体 hashcode 方法的场景。在 hashCode 方法中打断点(需要去除注解,自己手动重写)。
- 比较清晰的看到是 Collectors.toMap() 方法的 merge 时出现了异常
- 因为没有指定 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)); };
}
- 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() + ")";
}
- 最终会走到 Object.toString()。这里会进入 hashCode 。hashCode 方法会调用所有属性的hashCode 方法,那么由于实体是相互依赖的形式,就形成了hashCode调用的死循环,从而导致了堆栈溢出。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
结论
- 在使用 Collectors.toMap 方法时,一定要指定 merge 方法。
- 有相互 依赖关系的 尽量不要执行 toString 或者 hashCode 方法会造成死循环。
- 尽可能自己重写 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;
}
}
}