如何在 Java 中通过 重写 clone() 方法 解决对象复制过程中的引用溢出安全问题
Ja va 领域里常听到一个说法:重写 clone() 方法是为了解决“引用溢出安全问题”。但坦白讲,这个提法本身就有问题——因为 JVM 层面压根不存在“引用溢出”这个概念。很多人真正想表达的是:浅拷贝导致的可变对象共享,引发了数据污染、线程不安全或者封装性被破坏的一系列麻烦。这些问题的根源,是浅
Ja va 领域里常听到一个说法:重写 clone() 方法是为了解决“引用溢出安全问题”。但坦白讲,这个提法本身就有问题——因为 JVM 层面压根不存在“引用溢出”这个概念。很多人真正想表达的是:浅拷贝导致的可变对象共享,引发了数据污染、线程不安全或者封装性被破坏的一系列麻烦。这些问题的根源,是浅拷贝带来的引用别名(aliasing)风险,而不是什么溢出。

所以要理解这个问题,得先弄清楚 Object.clone() 默认干了什么。
浅拷贝才是罪魁祸首
Object.clone() 默认执行的是浅拷贝:它创建一个新对象,但只复制字段的值。对于基本类型,这没问题;但对于引用类型,它只复制了引用地址——新旧对象里的那个字段指向的是同一块堆内存。只要那个对象是可变的,修改就会互相污染。
举个例子:类里有一个 ArrayList 字段,克隆之后两个对象共用同一个列表实例。这时候如果多线程环境下没有做同步访问,ConcurrentModificationException 随时会冒出来,或者数据不一致;更糟糕的是,外部代码可以通过克隆体偷偷修改原对象的私有状态,封装性荡然无存。
正确重写 clone():深拷贝的必经之路
要真正切断引用关系,必须在 clone() 方法里对每一个可变引用字段手动创建独立副本——也就是深拷贝,而且得保证整个对象图都被递归隔离。
- 让类实现
Cloneable接口(它只是个标记,不提供任何方法) - 重写
public Object clone(),先调用super.clone()拿到基础副本 - 然后对每个可变引用字段(集合、数组、自定义对象等)单独调用其
clone()或构造新实例并复制内容 - 返回类型建议直接写成具体类(利用 Ja va 5+ 的协变返回类型),省得调用方再强转
一个典型实现长这样:
public class Person implements Cloneable {
private String name;
private ArrayList hobbies;
@Override
public Person clone() {
try {
Person cloned = (Person) super.clone();
// 深拷贝可变引用字段
cloned.hobbies = new ArrayList<>(this.hobbies); // 集合深拷贝(元素不可变时安全)
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Cloneable 已实现,不会发生
}
}
}
深拷贝的边界与陷阱
深拷贝不是万能的银弹,得结合场景小心设计:
- 不可变对象不需要深拷贝:像
String、Integer这种,直接赋值引用就好,它们自己就不会变。 - 循环引用需要特殊处理:如果对象图里有循环,递归 clone 会栈溢出。这时候得用缓存记录已经克隆过的对象,做引用映射。
- 第三方类未必支持 clone():如果某个字段的类型没实现
Cloneable或者没有公开的复制方式,那就得换路子——构造器、Builder 或者序列化方案。 - 性能开销不可忽视:深度遍历和复制大量对象会消耗时间和内存。高频场景下,对象池或者不可变设计可能是更好的选择。
更现代、更安全的替代方案
说回来,Cloneable 接口的设计本身就有硬伤:没有强制方法、异常机制模糊、还容易出错。所以业界早就推荐用更清爽的方式了:
- 拷贝构造器(Copy Constructor):比如
public Person(Person other),明确、类型安全,深浅拷贝逻辑自己控制。 - 静态工厂方法:比如
Person.copyOf(other),语义清晰,还能返回子类型。 - 不可变对象设计:用 final 字段加上无修改方法,天然就避免了共享修改的问题。
- 记录类(Record,Ja va 14+):自动提供不可变、值语义的浅拷贝,配合防御性复制(defensive copying)使用,简洁又安全。
说到底,与其费劲跟 clone() 的坑较劲,不如从一开始就用更现代的设计来规避风险。这才是真正可靠的“安全”之道。


































