Skip to content

创建型模式

约 7199 字大约 24 分钟

2025-10-23

单例模式

单例模式是一种创建型设计模式,它的核心目标是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。

在技术实现上,它通常通过以下两点来实现:

  1. 将类的构造方法私有化:防止外部使用 new 关键字随意创建对象。
  2. 在类内部创建唯一实例:并提供一个公共的静态方法(如 getInstance())让外部获取这个实例。

单例模式的应用场景

单例模式适用于那些需要且只需要一个实例来协调系统行为的场景。具体包括:

  1. 重量级资源对象(减少资源消耗)
  • 数据库连接池:创建数据库连接是昂贵的操作,使用连接池(单例)来管理复用连接,避免频繁创建和销毁。
  • 线程池:类似数据库连接池,用于管理线程资源。
  • 日志对象:应用程序通常只需要一个日志记录器实例来统一处理日志的写入和配置。
  1. 配置信息类(保证全局配置一致)
  • 应用程序的配置信息(如系统参数、设置项)在整个生命周期内应该只有一份,单例模式可以保证所有模块读取到的配置都是同一份,避免配置不一致。
  1. 工具类(方便管理)
  • 一些没有状态的工具类,如加密解密、日期处理等,也可以设计为单例,避免创建多个无意义的对象。
  1. 全局状态或缓存(共享数据)
  • 需要全局访问的缓存对象,如网站的商品分类缓存、用户会话信息管理器等。单例模式确保所有请求访问的是同一个缓存实例。
  1. Spring 框架中的 Bean
  • 在 Spring 框架中,默认情况下,由 IoC 容器管理的 Bean 的作用域就是单例(Singleton)。这意味着整个应用中,对于同一个 Bean 的请求,返回的都是同一个实例。

单例模式的实现方式

懒汉式(线程不安全)

该方式的特点是延迟加载,只有在调用getInstance()方法的时候才会去实例化这个单例对象。但是在多线程环境下,可能会创建多个单例对象。

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
     }
    return instance;
   }
}

懒汉式(同步方法)

通过synchornized关键字来保证实例话这个单例对象的线程安全。

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static synchronized Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
     }
    return instance;
   }
}

能够保证单例对象只有一个,但是性能较差。

懒汉式(双重检查锁)

通过volatilesynchornized关键配合使用,在获取锁前和获取锁都去检查这个对象是否被实例化过了。

public class Singleton {
  private static volatile Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
         }
       }
     }
    return instance;
   }
}

减少同步开销,兼顾线程安全和性能。

饿汉式

饿汉式(静态场景)

类加载即时创建实例,线程安全。它的线程安全是依赖于类加载机制,但是有可能一开始创建出来的这个单例对象,会在很后面才会被用到。前期创建出来得到单例对象就会造成资源的浪费。

public class Singleton {
  private static final Singleton instance = new Singleton();
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    return instance;
   }
}

饿汉式(静态内部类)

延迟加载且线程安全,它的线程安全是依赖于类加载机制。使用静态内部类的好处在于,Singleton类在加载的时候并不会去创建这个单例对象,而是在getInstance()方法被调用之后,才开始加载这个静态内部类,然后再去实例化这个单例对象。

public class Singleton {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
   }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
   }
}

枚举类实现单例

避免反射和序列化来破坏单例,由JVM保证实例的唯一性;

public enum Singleton {
  INSTANCE;
  // 可添加方法
  public void doSomething() { }
}

容器式单例

Spring的IOC容器,所有的对象都存储在一个Map中,然后所有需要使用这个对象的地方都不自己new这个对象,而是从这个Map中来获取这个单例对象。

因此这个对象只有在IOC容器实例化的时候才会去创建,后续所有使用这个对象的地方都是这个Map中来获取的,都是同一个实例对象。

注意Bean的Scope(作用域):Singleton、Prototype、Request、Session、Application和WebSocket。

单例模式的被破坏的场景

尽管我们通过单例模式的多种实现来保证了单例类只会有一个被实例化出来的对象,但是Java中仍然有很多的方式来创建出多个单例类的对象。但是只有枚举类实现的单例模式,是不会被反射、序列化/反序列化、克隆、多线程等方式破坏掉的。

反射

通过反射机制调用私有的构造方法来创建新的实例。

public class ReflectionBreakSingleton {
  public static void main(String[] args) throws Exception {
    Singleton instance1 = Singleton.getInstance();
    
    // 通过反射破坏单例
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true); // 突破私有权限
    Singleton instance2 = constructor.newInstance();
    
    System.out.println(instance1 == instance2); // false,不是同一个实例
   }
}

防止反射破坏单例模式,可以在构造方法中检查实例是否已经存在。

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {
    // 防止反射破坏
    if (instance != null) {
      throw new RuntimeException("单例模式禁止重复创建实例!");
     }
   }
}

序列化与反序列化

对象在经过序列化后反序列化,此时会创建新的对象。

public class SerializationBreakSingleton {
  public static void main(String[] args) throws Exception {
    Singleton instance1 = Singleton.getInstance();
    
    // 序列化
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
    oos.writeObject(instance1);
    oos.close();
    
    // 反序列化
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
    Singleton instance2 = (Singleton) ois.readObject();
    ois.close();
    
    System.out.println(instance1 == instance2); // false
   }
}

防止通过序列化或反序列化破坏单例模式,可以通过实现readResolve()方法来实现,原理是在反序列化的时候返回当前实例,而不是通过序列化后的内容来重新创建。

public class Singleton implements Serializable {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
     }
    return instance;
   }
  
  // 防止序列化破坏
  private Object readResolve() {
    return getInstance(); // 返回已有的单例实例
   }
}

克隆

如果单例类实现了clone接口,克隆操作会创建新的实例。

public class CloneBreakSingleton {
  public static void main(String[] args) throws Exception {
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = (Singleton) instance1.clone();
    
    System.out.println(instance1 == instance2); // false
   }
}

class Singleton implements Cloneable {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
     }
    return instance;
   }
  
  @Override
  protected Object clone() throws CloneNotSupportedException {
    return super.clone(); // 默认克隆会创建新对象
   }
}

防止克隆破坏单例模式就是主动去重写这个clone()方法,抛出异常或返回当前实例。

@Override
protected Object clone() throws CloneNotSupportedException {
  // 方法1:直接抛出异常
  throw new CloneNotSupportedException("单例模式禁止克隆!");
  
  // 方法2:返回当前实例(不推荐,违反克隆语义)
  // return getInstance();
}

类加载器不同

同一个类被不同的类加载器加载,会产生不同的实例。

工厂方法

工厂方法模式是一种创建型设计模式,其核心思想是:定义一个用于创建对象的接口(即工厂方法),但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

工厂方法模式的特点

优点:

  1. 良好的封装性: 客户端只需要关心所需产品对应的工厂,无需关心创建细节,甚至不需要知道具体产品类的类名。
  2. 极强的可扩展性: 如果需要增加一种新产品,只需要新增一个具体工厂类和具体产品类即可,完全符合“开闭原则”(对扩展开放,对修改关闭)。无需修改任何已有的工厂和产品代码。
  3. 解耦: 它将客户端代码与具体产品实现类分离开来,降低了系统的耦合度。客户端面向抽象编程(依赖于抽象工厂和抽象产品),而不是具体实现。
  4. 符合单一职责原则: 将对象的创建过程封装在专门的工厂类中,使得类的职责更加清晰。

缺点:

  1. 类的个数增多: 每增加一个新产品,就需要增加一个具体产品类和一个对应的具体工厂类,这会导致系统中类的个数成对增加,在一定程度上增加了系统的复杂性。
  2. 增加了系统的抽象性和理解难度: 引入了抽象层,要求开发者对于抽象工厂和抽象产品有更好的理解。

应用场景

在以下情况下,考虑使用工厂方法模式:

  1. 无法预知对象的确切类别及其依赖关系时。
  • 例如,一个物流管理系统,最初只处理卡车运输,但未来可能需要支持海运。使用工厂方法,可以轻松添加 ShipLogistics(轮船物流)和 Ship(轮船)类,而无需修改处理运输的核心逻辑。
  1. 希望用户能够扩展你的软件库或框架的内部组件。
  • 许多框架(如Spring)大量使用工厂方法模式,将对象的创建交给用户去配置和扩展。
  1. 希望将产品对象的创建过程与其使用过程解耦,以便复用这些对象。
  • 例如,在代码中多处需要创建数据库连接。通过使用连接工厂,可以统一管理连接的配置和创建逻辑,如果需要从MySQL切换到Oracle,只需修改工厂实现即可。
  1. 需要提供高度的灵活性。
  • 当某个类的创建过程非常复杂,或者依赖于系统配置、运行环境等因素时,使用工厂方法可以集中管理这些复杂性。

经典应用:

  • 日志记录器: 可以创建文件记录、数据库记录、控制台记录等不同的日志记录器。
  • 数据库连接工厂: 可以创建MySQL连接、Oracle连接、PostgreSQL连接等。
  • GUI 工具包: 在不同操作系统下(Windows, Mac)创建风格不同的按钮、复选框等控件。

工厂方式的实现形式

工厂方法模式主要包含以下4个角色:

  1. 抽象产品:定义了产品的接口。
  2. 具体产品:实现了抽象产品接口的类。
  3. 抽象工厂:声明了工厂方法,该方法返回一个抽象产品类型的对象。
  4. 具体工厂:实现了工厂方法,负责创建具体的产品对象。

经典实现

假如我们有一个的日志打印系统,它有三种打印日志的方式:控制台输出、文件输出和数据库输出。

image.png
image.png

然后在使用的时候,需要使用什么类型的产品就使用什么类型的工厂来实例化这个产品就可以了。

public class Client {
  public static void main(String[] args) {
    // 客户端只需要和抽象工厂、抽象产品打交道
    LoggerFactory factory = new FileLoggerFactory(); // 可配置或注入
    Logger logger = factory.createLogger();
    logger.log("这是一个测试消息");

    // 切换日志类型非常容易,只需改变具体工厂
    factory = new ConsoleLoggerFactory();
    logger = factory.createLogger();
    logger.log("这是另一个测试消息");
   }
}

参数化工厂方法

参数化工厂方法就是将抽象工厂和具体工厂进行组合,根据type参数来决定实例化哪个产品。

image.png
image.png
class ParametricLoggerFactory {
  // 静态工厂方法,通过参数决定创建哪种日志器
  public static Logger getLogger(String type) {
    if ("file".equalsIgnoreCase(type)) {
      return new FileLogger();
     } else if ("database".equalsIgnoreCase(type)) {
      return new DatabaseLogger();
     } else if ("console".equalsIgnoreCase(type)) {
      return new ConsoleLogger();
     } else {
      throw new IllegalArgumentException("不支持的日志类型: " + type);
     }
   }
}

然后在使用的时候只需要根据具体的type属性来实例化对应的产品就好了。

public class Client {
  public static void main(String[] args) {
    // 通过参数指定类型
    Logger logger1 = ParametricLoggerFactory.getLogger("file");
    logger1.log("文件日志");

    Logger logger2 = ParametricLoggerFactory.getLogger("console");
    logger2.log("控制台日志");
   }
}

使用参数化工厂的方式减少了类的数量,但是新增产品的时候需要修改参数化工厂方法的内部逻辑,这一点违背了开闭原则。

抽象工厂

抽象工厂模式的核心思想是:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

这里的关键词是“一系列相关对象”(也称为“产品族”)。它不再是工厂方法模式中只创建一种产品,而是创建一整套有内在联系的产品。

抽象工厂模式的特点

优点:

  1. 保证产品族的兼容性:这是最大的优点。抽象工厂确保客户端始终只使用同一个产品族中的对象。例如,它保证了所有UI控件都是同一主题(如macOS风格),不会混用Windows风格的按钮和macOS风格的文本框。
  2. 将客户端与具体类解耦:客户端代码只依赖于抽象工厂和抽象产品接口,完全不知道正在创建的具体产品是什么。这使得更换整个产品族变得非常容易。
  3. 符合开闭原则(对产品族而言):当需要增加一个新的产品族(如增加一个“北欧风格家具厂”)时,只需要增加新的具体工厂和一系列具体产品类,无需修改任何现有代码。

缺点:

  1. 难以支持新种类的产品(违反开闭原则):这是最主要的缺点。如果要在现有产品族中增加一个新的产品种类(比如在“家具工厂”里增加生产“电视柜”的能力),就需要修改抽象工厂接口,这将导致所有具体工厂类都需要被修改,非常不便。
  2. 类的个数急剧增多:由于涉及多个产品等级结构(产品种类)和多个产品族,会导致系统中类的个数成倍增加,增加了系统的复杂性。

应用场景

在以下情况下,考虑使用抽象工厂模式:

  1. 系统需要一系列相关的产品对象,并且希望它们协同工作。
  • GUI 工具包:这是最经典的例子。需要为不同操作系统(Windows, Mac, Linux)创建一整套UI控件,如按钮、复选框、文本框。确保所有控件风格一致。
  • 跨数据库访问层:系统需要支持多种数据库(MySQL, Oracle, SQL Server)。需要创建一整套相关的对象,如连接、命令、数据读取器。切换数据库时,只需更换整个工厂。
  1. 系统需要动态切换产品族。
  • 例如,一个游戏的角色皮肤系统,有“科幻”和“中世纪”两套皮肤(产品族)。玩家可以一键切换整套皮肤,包括角色服装、武器、坐骑等。
  1. 产品对象的创建过程需要与使用过程独立。
  • 希望将产品的创建、组合和表示过程分离。

抽象工厂的实现方式

抽象工厂模式通常包含以下角色:

  1. 抽象工厂:声明一组用于创建一族产品的方法,每个方法对应一种产品。
  2. 具体工厂:实现抽象工厂的接口,负责创建属于特定产品族的具体产品对象。
  3. 抽象产品:为每种产品声明接口。
  4. 具体产品:定义由具体工厂创建的产品对象,实现相应的抽象产品接口。
image.png
image.png

此时用户在使用的时候不需要关心要使用类型的工厂:

class UserService {
  private DatabaseFactory factory;
  public UserService(DatabaseFactory factory) { this.factory = factory; }

  public void doBusiness() {
    Connection conn = factory.createConnection();
    Command cmd = factory.createCommand();
    conn.connect();
    cmd.execute();
   }
}

UserService如果需要使用MySQL相关的Connection和Command的时候只需要在构造的时候传入MySQLFactory就可以了,同理要使用PostgresSQL只需要传入PostgresSQLFactory即可。

与工厂方法模式的区别

|特性|工厂方法模式|抽象工厂模式| |-|-|-| |核心焦点|创建一种产品|创建一族相关的产品| |工厂职责|一个工厂类只负责创建一个具体产品|一个工厂类负责创建多个属于同一族的具体产品| |产品维度|针对一个产品等级结构(纵向)|针对多个产品等级结构(纵向),并组合成产品族(横向)| |扩展方向|扩展新的具体产品很容易(增加新的具体工厂)|扩展新的产品族很容易,但扩展新的产品种类很困难|

关系:抽象工厂模式通常使用工厂方法模式来实现。 (例如,单独看Connection或单独来看Command,其实就是工厂方法模式)

建造者模式

建造者模式的核心思想是:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式的特点

优点:

  1. 良好的封装性,构建与表示分离:
  • 客户端无需知道产品内部组成的细节。将产品本身的创建业务逻辑封装在具体的建造者类中,而指挥者类则封装了产品的组装过程。
  1. 构建过程易于扩展,符合开闭原则:
  • 如果需要一种新的产品表示(配置),只需要增加一个新的具体建造者即可,无需修改原有代码。
  1. 可以精细控制构建过程:
  • 由于指挥者是分步骤调用建造者的方法,因此可以对构建过程进行更精细的控制,而不像工厂模式那样一步到位。
  1. 避免重叠构造器问题(Telescoping Constructor Pattern):
  • 当一个类有大量可选参数时,传统构造器需要提供多个重载版本,代码难以编写和维护。建造者模式通过链式调用方法,优雅地解决了这个问题。

缺点:

  1. 产品组成部分必须相似:
  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似。如果产品之间的内在差异很大,则不适合使用建造者模式。
  1. 增加了系统的复杂性:
  • 需要额外创建建造者类和指导者类,如果产品类型单一且内部不复杂,使用建造者模式反而会使系统变得臃肿。

应用场景

在以下情况下,考虑使用建造者模式:

  1. 创建复杂对象,其创建过程需要多个步骤,且这些步骤的顺序是固定的。
  • 例如:生成一封复杂的邮件(需要设置发件人、收件人、主题、正文、附件等),生成一份XML或JSON文档,组装一辆汽车等。
  1. 创建的对象有多种不同的配置或表示,但构建过程相似。
  • 例如:创建不同配置的电脑(游戏版、办公版)、点购不同搭配的套餐(套餐A、套餐B)、创建不同风格的报告等。
  1. 需要避免“重叠构造器”反模式。
  • 当一个类的构造器参数超过4个,且其中很多是可选参数时,使用建造者模式(特别是静态内部类形式)可以使客户端代码更清晰、更易读。

建造者模式的实现方式

建造者模式主要包含以下4个角色:

  1. 产品:被构建的复杂对象,包含多个组成部分。
  2. 抽象建造者:为创建一个产品对象的各个部件指定抽象接口,一般包含构建各个部件的方法和返回产品的方法。
  3. 具体建造者:实现抽象建造者接口,定义并明确它所创建的复杂对象的各个部件,并提供检索最终产品的方法。
  4. 指导者:负责安排复杂对象的建造次序,它隔离了客户与对象的生产过程。

标准的构造者模式

我们需要生产一台电脑的时候,需要调配不同的组件来进行组装。就可以构建不同类型的产品的建造者,最终由这个建造者来完成产品的生产。

image.png
image.png

此时客户端只需要通过ComputerTechnician这类就可以完成不同类型的电脑的生产。

public class Client {
  public static void main(String[] args) {
    // 创建指导者
    ComputerTechnician technician = new ComputerTechnician();

    // 创建游戏电脑
    ComputerBuilder gamingBuilder = new GamingComputerBuilder();
    technician.setBuilder(gamingBuilder);
    Computer gamingComputer = technician.buildComputer();
    System.out.println("游戏电脑: " + gamingComputer);

    // 创建办公电脑
    ComputerBuilder officeBuilder = new OfficeComputerBuilder();
    technician.setBuilder(officeBuilder);
    Computer officeComputer = technician.buildComputer();
    System.out.println("办公电脑: " + officeComputer);
   }
}

链式建造者(内部静态类)

通过在产品类的内部创建一个建造者类,让该建造类来完成类的建造。

public class Computer {

  private String cpu;
  private String ram;
  private String storage;
  private String gpu;

  @Override
  public String toString() {
    return "Computer{" +
        "cpu='" + cpu + '\'' +
        ", ram='" + ram + '\'' +
        ", storage='" + storage + '\'' +
        ", gpu='" + gpu + '\'' +
        '}';
   }

  public static class Builder {
    private String cpu;
    private String ram;
    private String storage;
    private String gpu;

    public Builder cpu(String cpu) {
      this.cpu = cpu;
      return this;
     }

    public Builder ram(String ram) {
      this.ram = ram;
      return this;
     }

    public Builder storage(String storage) {
      this.storage = storage;
      return this;
     }

    public Builder gpu(String gpu) {
      this.gpu = gpu;
      return this;
     }

    public Computer build() {
      Computer computer = new Computer();
      computer.ram = ram;
      computer.storage = storage;
      computer.gpu = gpu;
      return computer;
     }
   }
}

然后客户端在使用的时候就可以自由的搭配不同的组件。

  public static void main(String[] args) {
    Computer computer1 = new Computer.Builder().cpu("Intel i7").ram("32GB").storage("1T").gpu("1070Ti").build();
    System.out.println(computer1);
    Computer computer2 = new Computer.Builder().cpu("Amd R7").ram("64GB").storage("1T").gpu("1070Ti").build();
    System.out.println(computer2);
   }

原型模式

原型模式的核心思想是:用原型实例指定创建对象的种类,并且通过拷贝(克隆)这些原型来创建新的对象。

原型模式的特点

优点:

  1. 性能优良,逃避构造函数的约束
  • 当直接创建一个大而复杂的对象(如需要从数据库加载大量数据初始化)成本很高时,使用克隆(尤其是浅拷贝)可以避免重复耗时的初始化过程,性能提升显著。
  • 克隆不会调用类的构造方法,因此可以绕过构造方法中可能存在的权限检查、初始化限制等。
  1. 简化对象创建过程
  • 客户端无需知道对象创建的细节,也不需要关心对象的具体类名,只需知道如何克隆即可。
  • 对于包含大量可选参数和嵌套结构的复杂对象,克隆比一步步配置要方便得多。
  1. 动态获取对象运行时状态
  • 可以动态地保存和恢复对象的当前状态。你可以随时克隆一个对象,作为该对象在某一时刻的快照。

缺点:

  1. 克隆方法的复杂性
  • 每个需要克隆的类都必须实现克隆方法。当对象内部结构复杂,特别是存在循环引用时,实现一个正确无误的深拷贝会变得非常复杂和困难。
  • 需要仔细考虑是使用浅拷贝还是深拷贝,错误的选择可能导致意想不到的Bug。
  1. 违背“开闭原则”的风险
  • 如果原型对象在克隆过程中需要修改(例如,为深拷贝增加对新成员变量的处理),就必须修改原型类本身的代码,而不是通过扩展来完成。

应用场景

在以下情况下,考虑使用原型模式:

  1. 系统需要规避创建新对象的昂贵开销
  • 对象数据需要从数据库、网络等IO密集型操作中获取:例如,一个从数据库加载了10000条记录的对象,克隆它比重新查询数据库要快几个数量级。
  • 对象初始化过程非常复杂:例如,一个对象需要经过复杂的计算或依赖多个外部服务才能完成初始化。
  1. 一个系统需要独立于它的产品创建、构成和表示时
  • 比如,图形编辑器中的图形工具,你拖拽一个“圆形工具”到画布上,实际上就是克隆了一个预设的圆形原型。
  1. 需要避免使用分层次的工厂类来创建分层次的对象
  • 当类的数量非常多,且用工厂方法或抽象工厂会产生大量工厂类时,可以用原型模式来替代。通过注册和管理原型对象,可以动态地“生产”新对象。
  1. 需要保存对象状态,用于撤销/重做操作或状态回滚
  • 在需要实现撤销功能的应用中(如文档编辑器、绘图软件),可以通过克隆来保存对象的某个历史状态。执行撤销操作时,只需将对象恢复到之前克隆的状态即可。

经典应用:

  • Java Object 的 clone() 方法:是所有类的默认原型机制的基础。
  • Spring 框架中的原型作用域 Bean(prototype):每次请求都会返回一个新的克隆实例。
  • 细胞分裂模拟、游戏中的怪物生成:基于一个原型怪物,克隆出大量具有相同基础属性的怪物。

原型模式的实现方式

原型模式的核心是实现一个“克隆”自身的接口。在Java中,最直接的方式是实现 Cloneable 接口并重写 Object 类的 clone() 方法。关键在于理解浅拷贝与深拷贝的区别。

角色构成:

  1. 抽象原型类:声明克隆方法的接口,通常是 Cloneable 接口。
  2. 具体原型类:实现 Cloneable 接口,并重写 clone() 方法,实现具体的克隆逻辑。
  3. 客户端:通过调用具体原型类的 clone() 方法来创建新对象。

浅拷贝

浅拷贝只复制对象本身和基本数据类型字段,对于引用类型的字段,它只复制内存地址(即引用),因此原对象和新对象共享一个引用对象。修改其中一个对象的引用字段, 会影响另外一个对象。

image.png
image.png

对于Sheep只需要实现Cloneable接口就可以了:

 @Override
  public Sheep clone() {
    try {
      // 直接调用 Object 的 clone(),这是浅拷贝
      return (Sheep) super.clone();
     } catch (CloneNotSupportedException e) {
      throw new AssertionError(); // Cloneable 已实现,不会发生
     }
   }

客户端使用的时候可以通过clone()方法来获取新的对象:

  public static void main(String[] args) {
    // 创建原型羊 "Dolly"
    Sheep dolly = new Sheep("Dolly", 2, "White");
    System.out.println("原型羊: " + dolly);

    // 通过克隆创建新羊
    Sheep dollyClone = dolly.clone();
    dollyClone.setName("Dolly-Clone"); // 修改基本类型,不影响原型

    System.out.println("克隆羊: " + dollyClone);
    System.out.println("原型羊 after clone: " + dolly); // 原型羊的name没变

    // 测试浅拷贝的问题:修改克隆羊的羊毛颜色
    dollyClone.getFleece().setColor("Black");

    System.out.println("\n修改克隆羊的羊毛颜色后:");
    System.out.println("克隆羊: " + dollyClone); // 颜色变为 Black
    System.out.println("原型羊: " + dolly);   // !!!原型羊的颜色也变成了 Black!这就是浅拷贝的问题。
    System.out.println("原型和克隆的fleece是同一个对象吗? " + (dolly.getFleece() == dollyClone.getFleece())); // true
   }

深拷贝

深拷贝会复制对象本身以及它所包含的所有引用对象,一直到最基本的数据类型。原对象和克隆对象完全独立,互不影响。

方法A:在clone()方法中手动递归克隆引用对象

 @Override
  public Sheep clone() {
    try {
      // 1. 调用 super.clone() 完成基本数据类型的浅拷贝
      Sheep clonedSheep = (Sheep) super.clone();
      // 2. 对引用类型字段,也调用其 clone 方法进行克隆(递归)
      clonedSheep.fleece = this.fleece.clone(); // 假设 Fleece 也实现了深拷贝
      return clonedSheep;
     } catch (CloneNotSupportedException e) {
      throw new AssertionError();
     }
   }

方法B:通过序列化和反序列化来实现深拷贝

public Sheep deepCopy() {
    try (
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(bos);
     ) {
      oos.writeObject(this); // 将对象序列化到字节流
      oos.flush();

      try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
        return (Sheep) ois.readObject(); // 从字节流反序列化出新对象
       }
     } catch (IOException | ClassNotFoundException e) {
      throw new RuntimeException("深拷贝失败", e);
     }
   }

至于深拷贝的内容可以参考的浅拷贝的实现内容。