Java中toString方法默认不处理循环引用,直接调用会导致无限递归并抛出StackOverflowError;常见于双向链表、父子树结构、ORM级联实体等场景;安全方案包括跳过关联对象、ThreadLocal记录已遍历对象、使用Apache Commons Lang的循环检测或Jackson预处理。

Java 中的 toString 方法默认不会自动检测或处理循环引用,直接调用会导致无限递归,最终抛出 StackOverflowError。
为什么循环引用会引发栈溢出
当对象 A 持有对象 B 的引用,B 又反过来持有 A 的引用,且两者在 toString() 中都尝试打印对方时,就会形成调用链:A.toString → B.toString → A.toString → …,JVM 调用栈持续增长直至溢出。
常见场景示例
比如双向链表节点、父子关系的树结构、ORM 中的级联实体(如 User ↔ Department)等,若 toString 直接拼接关联对象,极易触发该问题。
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
- 未重写
toString:使用Object.toString()输出哈希码,不触发递归,但信息无意义 - 手动拼接字段(含引用对象):若未加控制,一调用就崩溃
- 用 Lombok 的
@ToString:默认不递归打印,但可通过exclude或of显式指定字段
安全处理循环引用的几种方式
核心思路是打破递归链,常用做法包括:
立即学习“Java免费学习笔记(深入)”;
- 避免在 toString 中访问可能形成闭环的引用字段:只打印基础属性(id、name 等),跳过关联对象或仅打印其 id
-
使用 ThreadLocal 记录已遍历对象:在
toString开始时检查当前对象是否已在调用栈中出现过,是则返回占位符(如"[CircularRef]") -
借助第三方库:如 Apache Commons Lang 的
ReflectionToStringBuilder支持循环引用检测(设置setCycleDetectionEnabled(true)) -
序列化前预处理:调试时可用 Jackson 的
ObjectMapper配合SimpleModule自定义序列化器,跳过或标记循环引用
一个轻量级防护示例
用 IdentityHashMap 跟踪递归深度(注意:仅限调试或低频调用场景,生产慎用):
private static final ThreadLocal<IdentityHashMap<Object, Void>> seen =
ThreadLocal.withInitial(IdentityHashMap::new);
public String toString() {
IdentityHashMap<Object, Void> map = seen.get();
if (map.containsKey(this)) {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this));
}
map.put(this, null);
try {
return String.format("%s{id=%d, name='%s', parent=%s}",
getClass().getSimpleName(), id, name, parent);
} finally {
map.remove(this);
if (map.isEmpty()) seen.remove();
}
}
不复杂但容易忽略

















