【软考设计模式】原型模式:“克隆“对象与深拷贝、浅拷贝精讲

系列定位:软考软件设计师 / 系统架构设计师 --- 创建型模式专题第 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",但 doc2prototype 的附件列表里也 莫名其妙地多出了这个文件

原因 :浅拷贝只复制了 attachments 这个 引用地址 ,原型、doc1doc2 三个对象指向的是 同一个 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 对象,并复制了所有元素。

软考要点 :如果原型类里有引用类型字段(如 ListMap、自定义对象),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() 方法里有没有对 ListMap、数组、自定义对象等引用类型字段做 new Xxx() 操作。如果没有 → 浅拷贝;如果有 → 深拷贝。

陷阱 4:混淆 clone()new

clone() 复制的是对象 当前的状态 (包括已经修改过的字段值),而 new 创建的是对象的 初始状态(由构造器决定)。如果题目描述是 "基于现有对象创建新对象,保留现有状态" → 选原型模式。

陷阱 5:与单例模式混淆

单例模式保证 只有一个实例 ;原型模式通过 clone() 可以产生 无数个实例 。两者是完全相反的思路。如果代码里既有 getInstance() 又有 clone(),不要混淆 ------ 前者是单例,后者是原型。

陷阱 6:深拷贝的递归问题

如果对象内部有多层引用嵌套(如 A 引用 BB 引用 C),手动深拷贝需要层层 clone()。软考通常只考一层引用(如 List),不会考多层嵌套。但如果遇到,思路是递归调用每个引用对象的 clone()


十一、总结

要点 内容
定义 用原型实例指定创建对象的种类,并通过复制这些原型创建新对象
分类 创建型模式(GoF 23 正式成员)
核心角色 Prototype(抽象原型)、ConcretePrototype(具体原型)、Client(客户端)
实现前提 实现 Cloneable 接口,重写 clone() 方法
浅拷贝 super.clone(),引用类型字段共享(修改相互影响)
深拷贝 super.clone() + 手动 new 引用对象并复制数据(完全独立)
原型管理器 Map 注册多个原型,客户端通过名称获取克隆,实现解耦
与工厂模式区别 原型通过 clone() 复制现有对象;工厂通过 new 全新创建
适用场景 创建成本高、需保留现有状态、需独立于产品创建过程
软考重点 深拷贝与浅拷贝的区分;clone() 方法补全;Cloneable 接口的作用;原型管理器的注册与获取
答题技巧 看到 clone() 方法、Cloneable 接口 → 原型模式;看到引用类型修改相互影响 → 浅拷贝问题

系列预告 :创建型模式五兄弟(简单工厂、工厂方法、抽象工厂、单例、建造者、原型)已全部讲完。下一篇将开启 结构型模式 篇章,从 适配器模式 开始 ------ 当新旧接口不兼容时,如何在不修改原有代码的前提下,让原本因接口不匹配而无法工作的类能够协同工作。咱们下回见。

相关推荐
geovindu1 小时前
java: Builder Pattern
java·开发语言·后端·设计模式·建造者模式·生成器模式
researcher-Jiang13 小时前
Design Patterns——Template Method入门到情景实战
python·设计模式·模板方法模式
饼干哥哥21 小时前
用Claude Code跑通跨境电商的5大场景,顶10个人的团队
人工智能·设计模式·正则表达式
折哥的程序人生 · 物流技术专研1 天前
第3篇:手写促销策略引擎(满减、打折、立减)
java·设计模式·策略模式·开闭原则·编程实战·扩充系列·促销引擎
折哥的程序人生 · 物流技术专研1 天前
第3篇:手写一个饮品制作模板(附代码)
java·设计模式·行为型模式·模版方法模式·钩子方法·代码复用·编程实战
ttod_qzstudio1 天前
【软考设计模式】单例模式:唯一实例的管控与线程安全精讲
单例模式·设计模式
折哥的程序人生 · 物流技术专研1 天前
第4篇:模板方法 vs 策略模式,面试怎么选?
java·设计模式·策略模式·行为型模式·模版方法模式·扩充系列·继承v组合
小园子的小菜1 天前
Java设计模式精讲:创建型+结构型模式(含生产级案例+相似模式对比)
学习·设计模式
geovindu2 天前
java:Abstract Factory Pattern
java·开发语言·后端·设计模式·抽象工厂模式·创建型模式