为何以下代码抛出java.lang.StackOverflowError?内部原理解析
Set s = new HashSet(); s.add(s); s.add(s); 会抛出 StackOverflowError? 这问题戳中了HashSet底层实现的一个有趣递归陷阱,咱们一步步拆解来看:
先搞懂HashSet的底层逻辑
HashSet本质是靠HashMap实现的——当你调用add()方法时,其实是把要添加的元素作为HashMap的key,value则是一个固定的空对象(PRESENT)。所以add()的核心逻辑依赖HashMap的put()方法,这个方法会做两件关键事:
- 计算key的哈希值(用来定位HashMap的桶位置)
- 检查是否已存在相同的key(通过哈希值+
equals()判断)
触发栈溢出的核心:无限递归计算哈希码
HashSet继承自AbstractSet,而AbstractSet的hashCode()方法实现是这样的:
public int hashCode() { int h = 0; Iterator<E> i = iterator(); while (i.hasNext()) { E obj = i.next(); if (obj != null) h += obj.hashCode(); } return h; }
简单说:集合的哈希码 = 集合内所有元素哈希码的总和。
现在看代码的执行流程:
Set s = new HashSet();→ 初始化一个空HashSet,内部的HashMap也是空的。s.add(s);→ 第一次添加自己。此时计算key(s)的哈希码:因为集合是空的,hashCode()遍历空集合后返回0,HashMap顺利把s作为key存入,此时集合里有了一个元素——就是s自己。s.add(s);→ 第二次添加自己。这时候需要先计算key(s)的哈希码:- 调用s的
hashCode(),遍历集合内的元素(也就是s自己) - 遍历到s时,需要计算它的哈希码,也就是再次调用s的
hashCode() - 又要遍历集合内的元素(还是s自己),再次触发
hashCode()调用 - 无限递归下去,栈的调用深度越来越大,最终超过JVM的栈容量,抛出
StackOverflowError
- 调用s的
从报错信息看递归链
你提供的报错信息已经清晰暴露了递归路径:
Exception in thread "main" java.lang.StackOverflowError
at java.util.HashMap$KeyIterator.<init>(HashMap.java:1459)
at java.util.HashMap$KeySet.iterator(HashMap.java:916)
at java.util.HashSet.iterator(HashSet.java:172)
at java.util.AbstractSet.hashCode(AbstractSet.java:122)
...
这条链的逻辑是:AbstractSet.hashCode() → 调用HashSet.iterator() → 获取HashMap的KeyIterator → 在hashCode()里遍历元素,调用元素的hashCode() → 又回到AbstractSet.hashCode(),循环往复直到栈溢出。
总结来说:把集合自身添加到集合后,计算集合哈希码时会触发无限递归,最终导致栈溢出。
内容的提问来源于stack exchange,提问作者Raj

