系列定位:软考软件设计师 / 系统架构设计师 --- 创建型模式专题第 5 讲
考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现
难度等级:⭐⭐⭐☆☆(概念简单,但深拷贝与浅拷贝的区分是高频失分点)
一、考纲定位与模式定义
1.1 考纲要求
原型模式在软考中属于 创建型模式 的基础内容。考察形式包括:
-
上午选择题 :判断场景描述所属模式;识别深拷贝与浅拷贝的区别;判断
clone()方法的使用规范;区分原型模式与工厂模式的适用场景 -
下午设计题 :补全原型类的
clone()方法实现;补全原型管理器(PrototypeManager)的注册与获取逻辑;识别类图中原型角色的典型特征(实现Cloneable接口 + 重写clone())
1.2 模式定义
原型模式:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
核心意图 :当直接创建对象的代价较大(如需要复杂初始化、需要从数据库/网络加载数据),或者需要保留现有对象的状态作为新对象的起点时,通过 "克隆"现有对象 来创建新对象,而不是从头 new 一个。
二、UML 类图与角色划分
┌─────────────────────────────────────────────────────────┐
│ Client │
│ + main() │
│ ├── prototype.clone() // 复制原型 │
│ └── 修改副本属性 │
└───────────────────────┬─────────────────────────────────┘
│ clones
▼
┌─────────────────────────────────────────────────────────┐
│ <<interface>> │
│ Prototype │
│ + clone(): Prototype │
└───────────────────────┬─────────────────────────────────┘
┌────────────┴────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ ConcretePrototypeA │ │ ConcretePrototypeB │
│ + clone() │ │ + clone() │
│ return copy │ │ return copy │
└─────────────────────┘ └─────────────────────┘
| 角色 | 职责 | 软考填空关键词 |
|---|---|---|
| Prototype(抽象原型) | 声明 clone() 接口,定义复制自身的规范 |
interface / abstract class + clone() |
| ConcretePrototype(具体原型) | 实现 clone() 方法,完成自身的复制(浅拷贝或深拷贝) |
implements Cloneable + public Object clone() |
| Client(客户端) | 通过调用原型的 clone() 方法创建新对象,再根据需求修改副本 |
prototype.clone() |
类图识别要点 :软考类图中,原型模式的核心特征是类实现
Cloneable接口(标记接口),并重写clone()方法返回自身副本。如果类图里有clone()方法且返回自身类型,大概率是原型模式。
三、场景一:浅拷贝原型(基础版,引用类型共享)
业务背景 :文档模板系统,每个新文档都基于一个 "标准模板" 复制而来,然后修改标题和作者。模板包含基本格式(字体、页边距)和附件列表(引用类型)。使用 浅拷贝 实现。
说明:浅拷贝是原型模式的最基础实现。它复制对象的基本类型字段,但 引用类型字段(如 List、数组、对象引用)只复制引用地址 ,新旧对象共享同一个引用对象。这是软考 最常考的陷阱点。
3.1 代码实现
java
import java.util.ArrayList;
import java.util.List;
public class DocumentTemplate implements Cloneable {
private String title;
private String font;
private List<String> attachments;
public DocumentTemplate(String title, String font) {
this.title = title;
this.font = font;
this.attachments = new ArrayList<>();
}
public void addAttachment(String file) {
attachments.add(file);
}
@Override
public DocumentTemplate clone() {
try {
return (DocumentTemplate) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException("克隆失败", e);
}
}
public void setTitle(String title) { this.title = title; }
public void setFont(String font) { this.font = font; }
@Override
public String toString() {
return "DocumentTemplate{title='" + title + "', font='" + font + "', attachments=" + attachments + "}";
}
}
public class Client {
public static void main(String[] args) {
DocumentTemplate prototype = new DocumentTemplate("标准模板", "宋体 12号");
prototype.addAttachment("公司Logo.png");
prototype.addAttachment("页眉模板.docx");
System.out.println("原型: " + prototype);
DocumentTemplate doc1 = prototype.clone();
doc1.setTitle("2024年Q2销售报告");
doc1.setFont("黑体 14号");
DocumentTemplate doc2 = prototype.clone();
doc2.setTitle("2024年Q3预算方案");
doc1.addAttachment("销售数据.xlsx");
System.out.println("doc1: " + doc1);
System.out.println("doc2: " + doc2);
System.out.println("原型: " + prototype);
}
}
3.2 运行结果与问题分析
原型: DocumentTemplate{title='标准模板', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx]}
doc1: DocumentTemplate{title='2024年Q2销售报告', font='黑体 14号', attachments=[公司Logo.png, 页眉模板.docx, 销售数据.xlsx]}
doc2: DocumentTemplate{title='2024年Q3预算方案', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx, 销售数据.xlsx]}
原型: DocumentTemplate{title='标准模板', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx, 销售数据.xlsx]}
问题暴露 :doc1 添加了一个附件 "销售数据.xlsx",但 doc2 和 prototype 的附件列表里也 莫名其妙地多出了这个文件!
原因 :浅拷贝只复制了 attachments 这个 引用地址 ,原型、doc1、doc2 三个对象指向的是 同一个 ArrayList 对象。任何一个对象修改这个列表,其他对象都会 "被传染"。
软考高频考点 :浅拷贝中,引用类型字段共享同一个对象,修改一个会影响所有副本。这是选择题和填空题的 必考陷阱。
四、场景二:深拷贝原型(解决引用类型共享问题)
业务背景 :同样是文档模板系统,但要求每个克隆出来的文档都有 独立的附件列表 ,互不影响。使用 深拷贝 实现。
说明:深拷贝不仅复制对象本身,还 递归复制对象内部所有的引用类型字段 ,确保新旧对象完全独立。实现方式有两种:① 手动逐个复制引用对象;② 利用序列化(Serializable)自动深拷贝。软考代码填空通常考 手动逐个复制。
4.1 代码实现(手动逐个复制引用对象)
java
import java.util.ArrayList;
import java.util.List;
public class DocumentTemplateDeep implements Cloneable {
private String title;
private String font;
private List<String> attachments;
public DocumentTemplateDeep(String title, String font) {
this.title = title;
this.font = font;
this.attachments = new ArrayList<>();
}
public void addAttachment(String file) {
attachments.add(file);
}
@Override
public DocumentTemplateDeep clone() {
try {
DocumentTemplateDeep copy = (DocumentTemplateDeep) super.clone();
copy.attachments = new ArrayList<>(this.attachments);
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException("克隆失败", e);
}
}
public void setTitle(String title) { this.title = title; }
public void setFont(String font) { this.font = font; }
@Override
public String toString() {
return "DocumentTemplateDeep{title='" + title + "', font='" + font + "', attachments=" + attachments + "}";
}
}
public class Client {
public static void main(String[] args) {
DocumentTemplateDeep prototype = new DocumentTemplateDeep("标准模板", "宋体 12号");
prototype.addAttachment("公司Logo.png");
prototype.addAttachment("页眉模板.docx");
System.out.println("原型: " + prototype);
DocumentTemplateDeep doc1 = prototype.clone();
doc1.setTitle("2024年Q2销售报告");
doc1.addAttachment("销售数据.xlsx");
DocumentTemplateDeep doc2 = prototype.clone();
doc2.setTitle("2024年Q3预算方案");
doc2.addAttachment("预算表.xlsx");
System.out.println("doc1: " + doc1);
System.out.println("doc2: " + doc2);
System.out.println("原型: " + prototype);
}
}
4.2 运行结果
原型: DocumentTemplateDeep{title='标准模板', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx]}
doc1: DocumentTemplateDeep{title='2024年Q2销售报告', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx, 销售数据.xlsx]}
doc2: DocumentTemplateDeep{title='2024年Q3预算方案', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx, 预算表.xlsx]}
原型: DocumentTemplateDeep{title='标准模板', font='宋体 12号', attachments=[公司Logo.png, 页眉模板.docx]}
结果分析 :每个对象的附件列表 完全独立 ,修改互不影响。深拷贝通过 new ArrayList<>(this.attachments) 创建了一个 全新的 List 对象,并复制了所有元素。
软考要点 :如果原型类里有引用类型字段(如
List、Map、自定义对象),clone()方法中必须 手动创建新的引用对象 并复制数据,否则就是浅拷贝。这是下午代码填空的核心判分点。
五、场景三:原型管理器(通过注册表管理多个原型)
业务背景 :系统中有多种文档模板(合同模板、报告模板、通知模板),客户端不需要知道具体模板类,只需要通过名称从 原型管理器 中获取并克隆。这相当于给原型模式加了一层 "工厂" 的封装。
说明:原型管理器是原型模式的一种扩展用法,软考下午题可能以 "根据名称获取克隆对象" 的形式考察。
5.1 代码实现
java
import java.util.HashMap;
import java.util.Map;
public interface DocumentPrototype extends Cloneable {
DocumentPrototype clone();
void display();
}
public class ContractTemplate implements DocumentPrototype {
private String templateName = "标准合同模板";
private String content = "(合同正文占位)";
@Override
public ContractTemplate clone() {
try { return (ContractTemplate) super.clone(); }
catch (CloneNotSupportedException e) { throw new RuntimeException(e); }
}
@Override
public void display() {
System.out.println("[合同模板] " + templateName + ": " + content);
}
public void setContent(String content) { this.content = content; }
}
public class ReportTemplate implements DocumentPrototype {
private String templateName = "季度报告模板";
private String header = "(报告页眉占位)";
@Override
public ReportTemplate clone() {
try { return (ReportTemplate) super.clone(); }
catch (CloneNotSupportedException e) { throw new RuntimeException(e); }
}
@Override
public void display() {
System.out.println("[报告模板] " + templateName + ": " + header);
}
public void setHeader(String header) { this.header = header; }
}
public class PrototypeManager {
private static final Map<String, DocumentPrototype> registry = new HashMap<>();
static {
registry.put("contract", new ContractTemplate());
registry.put("report", new ReportTemplate());
}
public static DocumentPrototype createDocument(String type) {
DocumentPrototype prototype = registry.get(type);
if (prototype == null) {
throw new IllegalArgumentException("未知模板类型: " + type);
}
return prototype.clone();
}
public static void register(String type, DocumentPrototype prototype) {
registry.put(type, prototype);
}
}
public class Client {
public static void main(String[] args) {
DocumentPrototype doc1 = PrototypeManager.createDocument("contract");
doc1.display();
DocumentPrototype doc2 = PrototypeManager.createDocument("contract");
doc2.display();
DocumentPrototype doc3 = PrototypeManager.createDocument("report");
doc3.display();
System.out.println("doc1 == doc2 ? " + (doc1 == doc2));
}
}
关键体会 :原型管理器将 "原型注册" 和 "克隆获取" 封装起来,客户端完全不需要 new 任何具体类,也不需要知道原型类的名字。这体现了 "依赖倒置原则" ------ 客户端依赖抽象原型接口,而不是具体类。
六、三种场景对比与演进思路
| 维度 | 场景一:浅拷贝原型 | 场景二:深拷贝原型 | 场景三:原型管理器 |
|---|---|---|---|
| 拷贝深度 | 浅拷贝(引用类型共享) | 深拷贝(引用类型独立) | 取决于注册的原型是浅还是深 |
| 引用类型处理 | 共享原引用 | 新建引用对象并复制数据 | 由具体原型类决定 |
| 是否有管理器 | ❌ 无 | ❌ 无 | ✅ 有(PrototypeManager) |
| 客户端耦合 | 依赖具体原型类 | 依赖具体原型类 | 依赖抽象原型接口 |
| 适用场景 | 对象无引用类型字段,或允许共享 | 对象有引用类型字段,要求完全独立 | 多原型统一管理,客户端解耦 |
| 软考考法 | 选择题(判断浅拷贝问题) | 下午填空(补全深拷贝逻辑) | 下午填空(补全注册表获取逻辑) |
七、浅拷贝 vs 深拷贝:核心对比
| 对比项 | 浅拷贝(Shallow Copy) | 深拷贝(Deep Copy) |
|---|---|---|
| 基本类型字段 | 复制值 | 复制值 |
| 引用类型字段 | 复制引用地址(共享对象) | 创建新对象并复制数据(完全独立) |
| 实现方式 | super.clone() 即可 |
super.clone() + 手动复制引用对象 |
| 性能 | 快 | 较慢(需递归复制) |
| 内存占用 | 低(共享引用对象) | 高(每个副本都有独立对象) |
| 软考考点 | 引用类型修改会相互影响 | 引用类型修改互不影响 |
| 典型问题 | 修改一个副本的 List,其他副本也变了 | 无共享问题,但实现复杂 |
软考记忆口诀:浅拷贝 "复制壳子不复制里子",深拷贝 "壳子和里子全复制"。
八、软考高频考点与易混淆辨析
8.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 创建型模式(GoF 23 正式成员) |
| 核心角色 | Prototype(抽象原型)、ConcretePrototype(具体原型)、Client(可选 PrototypeManager) |
| 实现前提 | 具体原型类必须实现 Cloneable 接口(标记接口),否则调用 clone() 会抛 CloneNotSupportedException |
| 浅拷贝问题 | 引用类型字段共享,修改一个会影响其他副本和原型 |
| 深拷贝实现 | super.clone() 后,对引用类型字段手动 new 新对象并复制数据 |
| 适用场景 | ① 创建对象成本高(需复杂初始化/IO)② 需要保留对象当前状态作为新起点 ③ 系统需要独立于产品的创建、组合和表示 |
| 与工厂模式区别 | 工厂模式:通过 new 创建全新对象;原型模式:通过 clone() 复制现有对象 |
8.2 易混淆辨析:原型模式 vs 工厂模式
| 对比项 | 原型模式 | 工厂方法 / 抽象工厂 |
|---|---|---|
| 创建方式 | 复制现有对象(clone()) |
全新创建(new) |
| 依赖关系 | 依赖现有对象的状态 | 不依赖现有对象,从零构建 |
| 对象状态 | 新对象初始状态与原型一致,可在此基础上修改 | 新对象按工厂逻辑初始化,状态由工厂决定 |
| 适用场景 | 对象状态复杂、复制比重建更快 | 对象创建逻辑需要抽象、多态 |
| 类图特征 | 有 clone() 方法 |
有 createXxx() / factoryMethod() 方法 |
关键区分 :如果类图里有
clone()方法,且类实现Cloneable→ 原型模式。如果类图里有createProduct()或factoryMethod()→ 工厂模式。
九、真题风格模拟与代码填空
模拟题 1(上午选择题)
以下关于原型模式的叙述中,正确的是()。
A. 原型模式属于结构型模式,通过复制现有对象来创建新对象
B. 在 Java 中,实现原型模式只需重写
clone()方法,无需实现Cloneable接口C. 浅拷贝会复制对象的所有字段,包括引用类型字段指向的对象,因此副本与原型完全独立
D. 原型模式适用于创建对象成本较高,或需要基于现有对象状态创建新对象的场景
答案:D
解析:
-
A 错误:原型模式属于 创建型 模式,不是结构型。
-
B 错误:Java 中调用
Object.clone()的前提是实现Cloneable接口,否则抛CloneNotSupportedException。 -
C 错误:浅拷贝只复制引用类型字段的 地址 ,不复制引用指向的对象,因此副本与原型 不独立(共享引用对象)。
-
D 正确:这是原型模式的核心适用场景。
模拟题 2(下午代码填空 --- 浅拷贝与深拷贝判断)
某系统使用原型模式实现工作流节点(WorkFlowNode)的复制。节点包含名称(
name,String 类型)和前置节点列表(prevNodes,List 类型)。以下代码为浅拷贝实现,请补全(1)~(2),使其变为 深拷贝。
java
java
import java.util.ArrayList;
import java.util.List;
public class WorkFlowNode implements Cloneable {
private String name;
private List<WorkFlowNode> prevNodes;
public WorkFlowNode(String name) {
this.name = name;
this.prevNodes = new ArrayList<>();
}
public void addPrevNode(WorkFlowNode node) {
prevNodes.add(node);
}
@Override
public WorkFlowNode clone() {
try {
WorkFlowNode copy = (WorkFlowNode) super.clone();
(1)______ = (2)______;
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
答案:
-
(1)
copy.prevNodes -
(2)
new ArrayList<>(this.prevNodes)
阅卷要点:
如果填
copy.prevNodes = new ArrayList<>(this.prevNodes)→ 满分。这是深拷贝 List 的标准写法,新建了 List 并复制了元素引用。如果填
copy.prevNodes = this.prevNodes→ 零分。这是浅拷贝,没有创建新 List。如果填
copy.prevNodes = new ArrayList<>(); copy.prevNodes.addAll(this.prevNodes)→ 满分 。等价于new ArrayList<>(this.prevNodes)。
模拟题 3(下午代码填空 --- 原型管理器)
某系统使用原型管理器管理多种形状(Shape)原型。请补全(1)~(4)。
java
interface Shape extends Cloneable {
Shape clone();
void draw();
}
class Circle implements Shape {
@Override public Circle clone() {
try { return (Circle) super.clone(); } catch (Exception e) { throw new RuntimeException(e); }
}
@Override public void draw() { System.out.println("绘制圆形"); }
}
class Rectangle implements Shape {
@Override public Rectangle clone() {
try { return (Rectangle) super.clone(); } catch (Exception e) { throw new RuntimeException(e); }
}
@Override public void draw() { System.out.println("绘制矩形"); }
}
class ShapeManager {
private static final Map<String, Shape> registry = new HashMap<>();
static {
registry.put("circle", new Circle());
registry.put("rectangle", new Rectangle());
}
public static Shape createShape(String type) {
Shape prototype = (1)______;
if (prototype == null) {
throw new IllegalArgumentException("未知形状: " + type);
}
return (2)______;
}
public static void register(String type, (3)______ prototype) {
registry.put(type, prototype);
}
}
public class Client {
public static void main(String[] args) {
Shape s1 = ShapeManager.createShape("circle");
Shape s2 = ShapeManager.createShape("circle");
System.out.println(s1 == s2); // 输出 (4)______
}
}
答案:
-
(1)
registry.get(type) -
(2)
prototype.clone() -
(3)
Shape -
(4)
false
阅卷要点:
(2) 必须填
prototype.clone(),不能填prototype。如果返回原型本身,所有客户端拿到的是同一个对象,违背了原型模式的初衷。(3) 必须是
Shape(抽象类型),体现面向接口编程。(4) 输出
false,因为clone()创建的是新对象,不是原对象。
十、常见陷阱与注意事项
陷阱 1:误认为原型是结构型模式
原型模式属于 创建型 模式,因为它解决的是 如何创建对象 的问题(通过复制)。软考上午题如果问"以下属于结构型模式的是",选项里出现"原型" 不能选。
陷阱 2:忘记实现 Cloneable 接口
Java 中 Object.clone() 是 protected 方法,且会检查对象是否实现了 Cloneable 标记接口。如果没实现就调用 clone(),会抛 CloneNotSupportedException。软考代码填空里,如果类声明为 implements Cloneable,那就是送分提示。
陷阱 3:深拷贝与浅拷贝判断失误
这是软考 最高频的失分点。
-
浅拷贝 :
super.clone()后直接返回。引用类型字段共享。 -
深拷贝 :
super.clone()后,对引用类型字段手动new新对象。
快速判断技巧 :看 clone() 方法里有没有对 List、Map、数组、自定义对象等引用类型字段做 new Xxx() 操作。如果没有 → 浅拷贝;如果有 → 深拷贝。
陷阱 4:混淆 clone() 与 new
clone() 复制的是对象 当前的状态 (包括已经修改过的字段值),而 new 创建的是对象的 初始状态(由构造器决定)。如果题目描述是 "基于现有对象创建新对象,保留现有状态" → 选原型模式。
陷阱 5:与单例模式混淆
单例模式保证 只有一个实例 ;原型模式通过 clone() 可以产生 无数个实例 。两者是完全相反的思路。如果代码里既有 getInstance() 又有 clone(),不要混淆 ------ 前者是单例,后者是原型。
陷阱 6:深拷贝的递归问题
如果对象内部有多层引用嵌套(如 A 引用 B,B 引用 C),手动深拷贝需要层层 clone()。软考通常只考一层引用(如 List),不会考多层嵌套。但如果遇到,思路是递归调用每个引用对象的 clone()。
十一、总结
| 要点 | 内容 |
|---|---|
| 定义 | 用原型实例指定创建对象的种类,并通过复制这些原型创建新对象 |
| 分类 | 创建型模式(GoF 23 正式成员) |
| 核心角色 | Prototype(抽象原型)、ConcretePrototype(具体原型)、Client(客户端) |
| 实现前提 | 实现 Cloneable 接口,重写 clone() 方法 |
| 浅拷贝 | super.clone(),引用类型字段共享(修改相互影响) |
| 深拷贝 | super.clone() + 手动 new 引用对象并复制数据(完全独立) |
| 原型管理器 | 用 Map 注册多个原型,客户端通过名称获取克隆,实现解耦 |
| 与工厂模式区别 | 原型通过 clone() 复制现有对象;工厂通过 new 全新创建 |
| 适用场景 | 创建成本高、需保留现有状态、需独立于产品创建过程 |
| 软考重点 | 深拷贝与浅拷贝的区分;clone() 方法补全;Cloneable 接口的作用;原型管理器的注册与获取 |
| 答题技巧 | 看到 clone() 方法、Cloneable 接口 → 原型模式;看到引用类型修改相互影响 → 浅拷贝问题 |
系列预告 :创建型模式五兄弟(简单工厂、工厂方法、抽象工厂、单例、建造者、原型)已全部讲完。下一篇将开启 结构型模式 篇章,从 适配器模式 开始 ------ 当新旧接口不兼容时,如何在不修改原有代码的前提下,让原本因接口不匹配而无法工作的类能够协同工作。咱们下回见。