解决循环引用导致的tostring无限递归,核心是用标识机制避免重复访问:可用threadlocal配合finally清理,或更安全的identityhashmap;也可采用有限深度展开、延迟字符串化,或直接使用apache commons/lombok等成熟工具。

在复写 toString 时遇到循环引用(比如 A 引用 B,B 又引用回 A),直接调用对象的 toString() 会导致无限递归,最终抛出 StackOverflowError。核心解决思路是:**不无条件递归展开所有引用字段,而是用标识机制检测已遍历对象,避免重复访问**。
使用 ThreadLocal 记录已访问对象
借助 ThreadLocal<set>></set> 在当前线程中维护一个“正在 toString 的对象集合”,每次进入 toString 前检查当前对象是否已在集合中。若已存在,返回占位符(如 "[Circular Reference]" 或类名加哈希码);否则加入集合,完成后再移除。
- 适合大多数单线程或线程池场景,开销小、逻辑清晰
- 注意必须在 finally 块中清理 Set,防止内存泄漏或误判
- 示例片段:
private static final ThreadLocal> visited = ThreadLocal.withInitial(HashSet::new);
public String toString() {
Set
if (set.contains(this)) return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this));
set.add(this);
try {
return "MyClass{field=" + (field != null ? field.toString() : "null") + "}";
} finally {
set.remove(this);
}
}
用 IdentityHashMap 替代 Set 实现更安全的引用跟踪
IdentityHashMap 按引用相等(==)判断键是否存在,比 HashSet 更可靠(避免重写了 equals 导致误判)。尤其适合含自定义 equals 的类参与循环引用的场景。
- 替换
ThreadLocal<set></set>中的HashSet为IdentityHashMap<object boolean></object>,key 是对象引用,value 任意(如Boolean.TRUE) - 仍需在 finally 中清理,但 key 比较更精准,不会因 equals 逻辑干扰检测
- 若对象生命周期长且频繁 toString,可考虑复用 Map 实例而非每次都 new
对引用字段做“有限深度展开”或“延迟字符串化”
不依赖全局状态,转而控制展开粒度:只对一级引用字段调用 toString,深层引用改用简洁表示(如类名+hashCode 或仅打印类型)。
- 例如:
"field=OtherClass@" + System.identityHashCode(field) - 或封装工具方法:
Objects.toString(field, "[not expanded]"),配合自定义策略限制递归层数 - 适合调试日志要求不高、或能接受部分信息省略的场景,实现最轻量
使用标准库或成熟工具类(如 Apache Commons / Lombok)
Apache Commons Lang 的 ToStringBuilder.reflectionToString() 默认启用循环检测;Lombok 的 @ToString 注解也支持 exclude 和 callSuper,并可通过 @ToString.Include 显式控制字段。
- 引入依赖后一行搞定:
return ToStringBuilder.reflectionToString(this); - Lombok 示例:
@ToString(exclude = {"parent", "children"})直接切断已知循环链 - 优势是免手写、经充分测试;缺点是增加依赖,且定制性略低
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











