99% 的人以为自己用了原型模式——其实踩的是 Java 浅拷贝的坑
99% 的人以为自己用了原型模式——其实踩的是 Java 浅拷贝的坑原型模式是 GoF 23 个模式里最被低估的一个也是 Java 里最容易被「以为懂但其实没懂」的一个。你写一段clone()以为实现了原型模式。代码 review 时同事给你点个赞。生产环境上线三个月某天凌晨告警——某个配置被改之后全局都变了。你 debug 三天才发现是一次浅拷贝埋的雷。这不是段子是我去年 review 真实代码时遇到的场景。原型模式被「以为懂」的程度远超其他模式。Java Cloneable 的三大坑Java 自带的Cloneable接口几乎是 Java 标准库里设计得最差的一类 API。它的设计目标是「让对象支持原型复制」但实现细节里有三个反直觉的坑坑一Cloneable 接口是空的没有 clone 方法Cloneable接口本身没有定义任何方法。你实现它相当于告诉 JVM「我这个类可以被复制」但具体的复制逻辑要靠 Object.clone() 来完成而 clone() 是 protected 的你必须自己重写为 public 才能被外部调用。这就导致了一个诡异的事实实现 Cloneable 不等于能用 clone()。你必须同时 1. 实现 Cloneable 接口标记 2. 重写 Object.clone() 为 public 3. 调用 super.clone() 而不是 new 自己少了任何一步行为都不对。90% 的代码只做了第 1 步和第 3 步第 2 步没做结果调用方根本调不到 clone()。坑二Object.clone() 是浅拷贝不是你想要的「复制」默认的super.clone()做的是浅拷贝。对象的字段被复制但引用类型的字段仍然指向同一个对象。java public class OrderConfig implements Cloneable { private String configName; private List whiteList; // 引用类型 private DiscountStrategy strategy; // 引用类型Override public OrderConfig clone() { try { return (OrderConfig) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } }}OrderConfig original new OrderConfig(); original.setWhiteList(Arrays.asList(user1, user2));OrderConfig copy original.clone(); copy.getWhiteList().add(user3);System.out.println(original.getWhiteList()); // 输出 [user1, user2, user3] // 你改了 copy但 original 也被改了 这才是绝大多数「原型模式」代码在生产环境翻车的真实原因。开发者写了 clone()以为对象是独立的运行期某个共享的可变集合被另一处修改所有副本集体出问题。坑三clone 不会调用构造函数super.clone()的实现是 JVM 内部 native 方法它直接分配内存然后把字段按位拷贝完全不走你的构造函数。这意味着 - 你在构造函数里做的字段校验不会跑 - 你用 Builder 模式构造对象时设置的 immutable 约束可能被绕过 - final 字段在 clone 时会出问题JVM 对 final 字段的写入有限制clone 走的是绕过构造函数的路径具体踩坑场景你写了一个Value风格的不可变对象所有字段 final构造函数抛 IllegalArgumentException if config invalid然后为了「方便」给它加了 clone()。某天调用方用 clone() 构造对象绕过了你的校验逻辑——因为构造函数根本没跑。真正能用的深拷贝三种方法对比如果你的对象真的需要原型模式独立的、不共享引用的副本深拷贝是逃不掉的。三种主流方法对比方法一递归 clone()每个引用类型字段都重写 clone()在父类的 clone() 里递归调用。java public class OrderConfig implements Cloneable { private List whiteList; private DiscountStrategy strategy;Override public OrderConfig clone() { try { OrderConfig copy (OrderConfig) super.clone(); copy.whiteList new ArrayList(this.whiteList); // 深拷贝 copy.strategy this.strategy.clone(); // 假设 DiscountStrategy 也支持 clone return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }} 优点性能最好没有序列化开销。 缺点每个类的字段都要手动维护一旦新增引用字段忘了 clone 就埋雷。继承链复杂时基本不可维护。方法二序列化反序列化java public class OrderConfig implements Serializable { // ... 字段 ...public OrderConfig deepClone() { try { ByteArrayOutputStream baos new ByteArrayOutputStream(); new ObjectOutputStream(baos).writeObject(this); ByteArrayInputStream bais new ByteArrayInputStream(baos.toByteArray()); return (OrderConfig) new ObjectInputStream(bais).readObject(); } catch (Exception e) { throw new RuntimeException(e); } }} 优点实现简单不用每个类重写 clone。继承链再深也无所谓。 缺点性能最差IO 反射 transient 处理。所有字段必须 Serializable。版本兼容性脆弱——改了 serialVersionUID 之后老的序列化数据就读不出来了。方法三JSON 序列化java public OrderConfig deepClone() { String json new Gson().toJson(this); return new Gson().fromJson(json, OrderConfig.class); }优点跨语言调试方便。 缺点性能比 Java 序列化还差。会丢失类型信息子类字段丢失。对循环引用直接挂。生产环境几乎不推荐。实际项目中我的偏好能改成不可变对象就别用 clone。不可变对象天然没有「需要深拷贝」的场景从根上避免。如果必须可变用方法一但只在性能敏感且字段稳定的核心对象上用。其他场景老老实实用方法二。Spring 的 scopeprototype 不是原型模式另一个高频误解很多人看到 Spring 配置文件里写bean scopeprototype/以为这就是在用 GoF 的原型模式。这是两个完全不同的概念。GoF 原型模式的核心是「通过复制现有对象来创建新对象」——目的是避免昂贵的初始化。Spring 的scopeprototype是「每次 getBean() 都创建一个新的对象实例」——本质上是 Bean 的生命周期管理跟复制不复制没半毛钱关系。Spring 的 prototype scope 内部实现是每次调用都走一遍完整的 Bean 创建流程实例化、属性注入、初始化回调——这是「重新构造」不是「复制」。如果你的 Bean 创建代价很贵要查数据库、要加载远程配置、要初始化大对象图想用 Spring 的 prototype scope 来「复制」已经存在的实例——这是行不通的。你需要的不是 scopeprototype而是真正的原型模式自己实现 Bean 的复制逻辑把它放进 BeanFactory 的 scope 定义里。很多团队在这上面浪费了时间。以为是 Bean scope 没配对其实是模式本身就没搞清楚。什么时候真正该用原型模式我做了十年 Java 后端真正用原型模式的场景不超过五种对象初始化代价远高于复制代价——比如一个从数据库加载的复杂配置对象、一个经过大量计算生成的算法参数对象。重新生成一次要 200ms复制只要 2ms。对象状态需要独立演化——同一份初始配置创建出 N 个独立副本每个副本各自修改互不影响。这是原型模式的核心价值跟浅拷贝的差别就在这里。避开构造函数的限制——比如对象需要通过 protected 构造函数或工厂方法创建但使用方需要复制它的能力。clone() 是绕过这些限制的合法路径。缓存场景——从缓存里取出的对象直接返回给调用方有共享风险用 clone() 隔离一份给调用方。这在 Spring 的 AbstractBeanDefinition 等核心类的实现里能看到。测试夹具准备——单元测试里要构造一个复杂的领域对象每次都重新 Builder 太麻烦写个 clone() 复制一个已有的「标准 fixture」即可。如果你遇到的不是这五种场景不要硬套原型模式。大多数情况下 - 想要独立副本改成不可变对象 - 想要简化创建用 Builder 模式 - 想要动态切换实现用工厂方法 - 想要共享状态这本来就不该用原型模式写在最后原型模式不是 Java 里的Cloneable接口加super.clone()。这是一个完整的工程决策——你需要想清楚为什么复制比构造更划算、需要解决浅拷贝带来的共享问题、需要维护复制逻辑的一致性。下次你在代码里写implements Cloneable之前先问自己三个问题这个对象的初始化代价真的高于复制代价吗我是不是只想避免重写 Builder想清楚 Builder 可能更合适。复制之后的对象生命周期是谁负责共享引用的边界画清楚了吗答不清楚就别用。我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」23 个设计模式用漫画 答题的方式讲目前正在开发中。如果你觉得这类内容有意思搜一下「爪爪代码冒险记」或者等我后面的文章。