1. 引言
在开发中,我们经常需要复制一个对象。看似简单的「复制」,实际上有两种截然不同的语义:浅拷贝(Shallow Copy) 与 深拷贝(Deep Copy)。理解它们的区别,是避免「改了副本却把原对象也改了」这类诡异 Bug 的关键。
本文从内存模型出发,讲清两者的本质区别,并给出 Java、Python、JavaScript 三种语言中的实现方式与常见坑点。
2. 从内存模型说起
一个对象在内存中通常由两部分组成:
- 对象本身:存放在栈或堆上的引用变量,以及对象头、基本类型字段等。
- 引用字段指向的对象:例如对象里的数组、列表、另一个对象,它们存放在堆的另一块区域。
拷贝时,如果只复制了「对象本身」,而引用字段仍然指向同一块内存,那就是浅拷贝;如果连引用字段指向的对象也递归复制了一份,那就是深拷贝。
#mermaid-svg-7AIG2BSEXXCJtjOq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7AIG2BSEXXCJtjOq .error-icon{fill:#552222;}#mermaid-svg-7AIG2BSEXXCJtjOq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7AIG2BSEXXCJtjOq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7AIG2BSEXXCJtjOq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7AIG2BSEXXCJtjOq .marker.cross{stroke:#333333;}#mermaid-svg-7AIG2BSEXXCJtjOq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7AIG2BSEXXCJtjOq p{margin:0;}#mermaid-svg-7AIG2BSEXXCJtjOq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq .cluster-label text{fill:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq .cluster-label span{color:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq .cluster-label span p{background-color:transparent;}#mermaid-svg-7AIG2BSEXXCJtjOq .label text,#mermaid-svg-7AIG2BSEXXCJtjOq span{fill:#333;color:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq .node rect,#mermaid-svg-7AIG2BSEXXCJtjOq .node circle,#mermaid-svg-7AIG2BSEXXCJtjOq .node ellipse,#mermaid-svg-7AIG2BSEXXCJtjOq .node polygon,#mermaid-svg-7AIG2BSEXXCJtjOq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7AIG2BSEXXCJtjOq .rough-node .label text,#mermaid-svg-7AIG2BSEXXCJtjOq .node .label text,#mermaid-svg-7AIG2BSEXXCJtjOq .image-shape .label,#mermaid-svg-7AIG2BSEXXCJtjOq .icon-shape .label{text-anchor:middle;}#mermaid-svg-7AIG2BSEXXCJtjOq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7AIG2BSEXXCJtjOq .rough-node .label,#mermaid-svg-7AIG2BSEXXCJtjOq .node .label,#mermaid-svg-7AIG2BSEXXCJtjOq .image-shape .label,#mermaid-svg-7AIG2BSEXXCJtjOq .icon-shape .label{text-align:center;}#mermaid-svg-7AIG2BSEXXCJtjOq .node.clickable{cursor:pointer;}#mermaid-svg-7AIG2BSEXXCJtjOq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7AIG2BSEXXCJtjOq .arrowheadPath{fill:#333333;}#mermaid-svg-7AIG2BSEXXCJtjOq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7AIG2BSEXXCJtjOq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7AIG2BSEXXCJtjOq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7AIG2BSEXXCJtjOq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7AIG2BSEXXCJtjOq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7AIG2BSEXXCJtjOq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7AIG2BSEXXCJtjOq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7AIG2BSEXXCJtjOq .cluster text{fill:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq .cluster span{color:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7AIG2BSEXXCJtjOq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7AIG2BSEXXCJtjOq rect.text{fill:none;stroke-width:0;}#mermaid-svg-7AIG2BSEXXCJtjOq .icon-shape,#mermaid-svg-7AIG2BSEXXCJtjOq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7AIG2BSEXXCJtjOq .icon-shape p,#mermaid-svg-7AIG2BSEXXCJtjOq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7AIG2BSEXXCJtjOq .icon-shape .label rect,#mermaid-svg-7AIG2BSEXXCJtjOq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7AIG2BSEXXCJtjOq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7AIG2BSEXXCJtjOq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7AIG2BSEXXCJtjOq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 深拷贝
原对象 A
引用字段
副本 B
引用字段
子对象 1
子对象 2
浅拷贝
原对象 A
引用字段
副本 B
共享的子对象
3. 浅拷贝与深拷贝的核心区别
| 对比项 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 基本类型字段 | 复制值 | 复制值 |
| 引用类型字段 | 复制引用,指向同一对象 | 递归复制,指向新对象 |
| 内存开销 | 小 | 大 |
| 执行速度 | 快 | 慢 |
| 相互影响 | 修改子对象会互相影响 | 完全独立,互不影响 |
| 适用场景 | 对象结构简单、只读共享 | 需要完全隔离的副本 |
一句话总结:浅拷贝只复制「一层」,深拷贝复制「整棵树」。
4. Java 中的实现
4.1 浅拷贝:clone() 与拷贝构造器
Java 中实现 Cloneable 接口并重写 clone(),默认就是浅拷贝:
java
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class User implements Cloneable {
String name;
Address address;
User(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public User clone() {
try {
return (User) super.clone(); // 浅拷贝
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
public class ShallowDemo {
public static void main(String[] args) {
User origin = new User("Tom", new Address("Beijing"));
User copy = origin.clone();
copy.address.city = "Shanghai";
System.out.println(origin.address.city); // Shanghai,原对象被影响
}
}
可以看到,copy.address 与 origin.address 是同一个对象,修改副本的地址,原对象也跟着变了。
4.2 深拷贝:手动递归克隆
在 clone() 中把引用字段也克隆一份,就变成了深拷贝:
java
class Address implements Cloneable {
String city;
Address(String city) {
this.city = city;
}
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
class User implements Cloneable {
String name;
Address address;
User(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public User clone() {
try {
User copy = (User) super.clone();
copy.address = this.address.clone(); // 关键:子对象也克隆
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
4.3 深拷贝:序列化方式
当对象层级很深时,手动递归克隆非常繁琐。可以借助序列化实现深拷贝:
java
import java.io.*;
public class DeepCopyUtil {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T obj) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
oos.flush();
try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("深拷贝失败", e);
}
}
}
注意:使用序列化方式时,对象及其所有引用字段都必须实现 Serializable。
5. Python 中的实现
Python 的 copy 模块直接提供了两种拷贝:
python
import copy
origin = {"name": "Tom", "tags": ["a", "b"]}
shallow = copy.copy(origin)
deep = copy.deepcopy(origin)
shallow["tags"].append("c")
print(origin["tags"]) # ['a', 'b', 'c'],浅拷贝影响了原对象
deep["tags"].append("d")
print(origin["tags"]) # ['a', 'b', 'c'],深拷贝互不影响
需要特别注意:切片、list()、dict() 构造、copy.copy() 都是浅拷贝。
python
a = [[1, 2], [3, 4]]
b = a[:] # 浅拷贝
b[0][0] = 99
print(a) # [[99, 2], [3, 4]],原列表被修改
6. JavaScript 中的实现
6.1 浅拷贝
javascript
const origin = { name: "Tom", tags: ["a", "b"] };
const shallow1 = Object.assign({}, origin);
const shallow2 = { ...origin };
shallow1.tags.push("c");
console.log(origin.tags); // ['a', 'b', 'c']
Object.assign 与展开运算符都只复制第一层属性,嵌套对象仍然是共享引用。
6.2 深拷贝
现代浏览器与 Node.js 已原生支持 structuredClone:
javascript
const origin = { name: "Tom", tags: ["a", "b"], date: new Date() };
const deep = structuredClone(origin);
deep.tags.push("c");
console.log(origin.tags); // ['a', 'b'],互不影响
如果环境不支持,可以用 JSON.parse(JSON.stringify(obj)) 兜底,但它有局限:会丢失 undefined、函数、Symbol,Date 会变成字符串,NaN 会变成 null,也无法处理循环引用。
javascript
// 简易递归深拷贝,支持数组与普通对象
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (map.has(obj)) return map.get(obj); // 处理循环引用
const result = Array.isArray(obj) ? [] : {};
map.set(obj, result);
for (const key of Object.keys(obj)) {
result[key] = deepClone(obj[key], map);
}
return result;
}
7. 常见坑点与最佳实践
- 不可变对象无需拷贝 :
String、Integer等不可变类型,赋值本身就是安全的。 - 集合的浅拷贝陷阱 :
List的new ArrayList<>(list)只复制了元素引用,元素对象仍是共享的。 - 循环引用 :递归深拷贝必须用
WeakMap或访问标记记录已拷贝对象,否则会栈溢出。 - 性能权衡:深拷贝开销大,能用不可变数据结构或只读视图解决时,优先避免深拷贝。
- 明确语义:对外暴露的 API 若返回内部集合,应返回副本或不可变视图,避免调用方意外修改内部状态。
8. 总结
- 浅拷贝:只复制对象本身,引用字段仍指向同一块内存,速度快但存在共享风险。
- 深拷贝:递归复制整棵对象树,完全隔离,但开销更大、实现更复杂。
- 选择哪种方式,取决于你是否需要副本与原对象完全独立。
记住一句话:拷贝前先问自己------我要的是「另一个引用」,还是「另一份数据」?