Skip to content
Go back

原型模式——对象克隆的深浅之道

原型模式:对象克隆的深浅之道

一句话结论(30s)

原型模式的本质是以空间换时间——用多一份内存换取对象创建的零成本,因为 Object.clone() 按字节复制已有对象、省去重新构造的过程,适合大量相似对象场景。核心权衡在浅拷贝与深拷贝:浅拷贝只复制第一层导致嵌套对象引用共享,深拷贝递归复制所有引用换来完全隔离但性能更慢。

核心原理(2min)

主流程:以已有对象为原型 → super.clone() 按字段字节拷贝 → 得到新对象。关键机制是 Cloneable 是空标记接口,Object.clone() 在 native 层检查 instanceof Cloneable、不满足就抛 CloneNotSupportedException;浅拷贝对基本类型复制值、对引用类型复制引用地址(嵌套对象共享),深拷贝需手动递归 clone 或用序列化/JSON 重建整个对象图。源码落地:HashMap.clone() 重新构建桶数组但 value 引用仍共享(浅拷贝);Spring 的 scope="prototype" 每次 getBean() 返回新实例,但底层用 CGLIB/反射而非 Cloneable,因为它要的是「同类型新对象」而非「同数据副本」。

底层深入(5-10min)

为什么需要克隆?

创建对象的成本有时远超你的想象。一个 HashMap 中存了 10 万条数据,你不想”从零构造”一个同样的 Map——你希望”复制一份,改几个 key”。这就是原型模式(Prototype Pattern)的核心场景:以已有对象为原型,通过复制创建新对象,避免昂贵的构造过程。

思考穿插:clone 到底比 new 快多少?为什么能快? 因为 new 要走完整的构造流程——分配内存、初始化字段、执行构造器(可能有昂贵 I/O、网络、复杂计算);而 Object.clone() 是 native 层按字节把已有对象的内存镜像复制一份,跳过了构造器,直接把「成品状态」搬过来。所以当对象「创建代价高、且已有相似样本」时,clone 才划算;如果构造本来就轻(一个 new Object()),clone 反而可能因为 native 调用开销而没优势——快的前提是「构造很贵」。

Cloneable:Java 中最反直觉的接口

public interface Cloneable {
    // 空的!一个方法都没有。
}

Cloneable 是一个标记接口(Marker Interface)——它不定义任何方法,只是告诉 JVM:“这个类的实例可以被克隆”。真正执行克隆的是 Object.clone()(native 方法)。

Object.clone() 有一个变态的设计:如果你不实现 Cloneable 就调用它 → 抛 CloneNotSupportedException。也就是说,JVM 在 native 层检查 instanceof Cloneable,不满足就抛异常——这违反了”接口 = 契约”的基本直觉。

public class User implements Cloneable {
    private String name;
    private Address address;  // 引用类型

    @Override
    public User clone() {
        try {
            return (User) super.clone();  // native方法,按字节拷贝
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

真实源码:Object.clone() 与 Cloneable

JDK 里 Cloneable 的定义(java.lang.Cloneable)确实干干净净——只有一个空壳:

public interface Cloneable {
}

真正干活的是 Object.clone()java.lang.Object),一个 protected 的 native 方法:

    @IntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

JDK 官方在 Object.clone()@implSpec 里把浅拷贝说得很直白:它创建一个同类新实例,把所有字段的内容「按赋值方式」原样复制过去,字段本身不会被再次克隆——原文即 “performs a ‘shallow copy’ of this object, not a ‘deep copy’ operation”。这正是前面 user2.address.city 一改、user1 也跟着变的根因:clone() 复制的是引用地址,而不是引用指向的对象。而 CloneNotSupportedException 的来源也在 javadoc 里写死——若对象的类没有实现 Cloneable,就直接抛这个异常。

浅拷贝 vs 深拷贝:一次面试必问的分水岭

浅拷贝(Shallow Copy)

Object.clone() 做的是按字节拷贝(field-by-field copy)

User user1 = new User("张三", new Address("北京"));
User user2 = user1.clone();

user2.name = "李四";                       // 改 String:OK,不影响 user1(String 不可变)
user2.address.city = "上海";               // 改 address:user1.address.city 也变成了"上海"!

原因:clone()address 的引用地址原样复制了一份。user1.addressuser2.address 指向堆上的同一个 Address 对象

HashMap.clone() 为什么是浅拷贝?

HashMap<String, Integer> map1 = new HashMap<>();
map1.put("a", 1);
HashMap<String, Integer> map2 = (HashMap<String, Integer>) map1.clone();

map2.put("b", 2);  // 新增 key → 不影响 map1(table 数组是新 copy 的)
map2.put("a", 100); // 改已存在的 value → 也不影响 map1(Integer 不可变)

等等,put("a", 100) 不会影响 map1?因为 Integer 是不可变对象——put 实际上是把新 Node 放进了新的 table 数组(clone() 内部重新构建了桶数组)。但如果 value 是可变对象:

HashMap<String, List<String>> map1 = new HashMap<>();
map1.put("users", new ArrayList<>(Arrays.asList("张三")));
HashMap<String, List<String>> map2 = (HashMap<String, List<String>>) map1.clone();

map2.get("users").add("李四");  // 修改 list 的内容
// map1.get("users") 现在也是 ["张三", "李四"] —— 因为引用共享!

clone() 复制了 Node 数组,但每个 Node 里的 value 引用还是指向同一个 ArrayList。这就是浅拷贝的本质:只复制第一层,嵌套对象引用共享。

思考穿插:浅拷贝和深拷贝的分水岭到底在哪? 一句话——「引用字段是复制引用地址,还是复制引用指向的对象」。浅拷贝只复制第一层:基本类型复制值(独立)、引用类型复制地址(共享);深拷贝要递归地复制每一层引用指向的对象,直到最底层都是基本类型或不可变对象。所以 user2.address.city 一改 user1 跟着变,是浅拷贝;序列化/JSON 重建后互不影响,是深拷贝。代价也很直观:深拷贝要复制整张对象图,性能更慢,换来的是完全隔离。

深拷贝(Deep Copy)

深拷贝要求递归复制所有引用对象,直到最底层都是基本类型或不可变对象。

方案一:手动递归 clone

@Override
public User clone() {
    User cloned = (User) super.clone();
    cloned.address = this.address.clone();  // Address 也要实现 Cloneable
    return cloned;
}

缺点:每个嵌套类都要实现 Cloneable 并重写 clone(),深层嵌套时代码冗长且易遗漏。

方案二:序列化实现深拷贝(通用方案)

public static <T> T deepCopy(T obj) {
    try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (T) ois.readObject();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

原理:序列化会将整个对象图(包括所有嵌套引用)写入字节流,反序列化再从字节流重建整个对象图 → 所有引用都是全新的。条件是所有涉及的类型都实现 Serializable

方案三:JSON 序列化(更轻量,但类型信息不完整)

String json = objectMapper.writeValueAsString(original);
User deepCopied = objectMapper.readValue(json, User.class);

原型模式在 Spring 中的应用

Spring 的 Bean Scope 中,scope="prototype" 就是原型模式:每次 getBean() 都返回一个新的实例(而非单例)。底层用的是 CGLIB 或反射创建新实例,而非 Cloneable——因为 Spring 需要的是”同类型的新对象”,不是”同数据的副本”。

总结

浅拷贝深拷贝
实现Object.clone()序列化 / 手动递归
基本类型字段独立独立
引用类型字段共享同一对象递归复制,完全独立
性能
安全性一个改了另一个也改完全隔离

原型模式的本质是以空间换时间——用多一份内存换取对象创建的零成本。在需要大量相似对象(如游戏中的子弹、GUI 中的图形组件)时,clone 比 new 快一个数量级。

章末提问

Q1:Cloneable 为什么是一个空接口,它到底干了什么? 结论先行:它是标记接口,本身不定义方法,作用是让 Object.clone() 在 native 层通过 instanceof Cloneable 判断是否允许克隆。因为真正干活的是 Object.clone()(native),Cloneable 只是「我声明可以克隆」的标记,不实现它就调 clone 会抛 CloneNotSupportedException——这违反「接口=契约」的直觉,是 Java 历史设计里的一个反直觉点。

Q2:浅拷贝和深拷贝的本质区别? 结论先行:区别在「引用字段复制的是引用地址,还是引用指向的对象」。因为浅拷贝只复制第一层,嵌套对象的引用被共享,一个改了另一个也改;深拷贝递归复制整张对象图,所有引用都指向全新对象,完全隔离。代价上深拷贝更慢。

Q3:为什么 map1.clone()map2.put("a", 100) 不影响 map1? 结论先行:因为 HashMap 的 clone 重新构建了桶数组,且 Integer 是不可变对象。put 是往新 table 数组里放新 Node,改的是 map2 自己的结构;而 value 是 Integer(不可变),不会发生「共享对象被原地修改」。反过来,若 value 是可变对象如 ArrayList,两边共享同一个 list,改了才会互相影响。

Q4:Spring 的 scope="prototype" 是原型模式吗?为什么不用 Cloneable? 结论先行:是原型模式的思想,但底层不用 Cloneable。因为 Spring 要的是「同类型的新对象」,不是「同数据的副本」,用 CGLIB 或反射直接创建新实例即可;Cloneable 是「复制数据」的语义,与 Bean 的语义不符。

Q5:深拷贝有哪几种实现,各自代价? 结论先行:手动递归 clone、序列化、JSON 三种,代价依次是「代码冗长易漏 / 慢且要求 Serializable / 轻量但类型信息可能不全」。因为手动递归要每个嵌套类都实现 Cloneable;序列化要全类实现 Serializable 且开销大;JSON 快但会丢泛型、类型信息,特殊类型(日期等)需额外配置。


Share this post on:

Previous Post
建造者模式——复杂对象构建的优雅之道
Next Post
单例模式的5种写法——从饿汉到枚举的演进