首页 > 其他分享 >《深入剖析原型模式:浅克隆、深克隆与单例模式的碰撞》

《深入剖析原型模式:浅克隆、深克隆与单例模式的碰撞》

时间:2024-08-20 14:51:21浏览次数:8  
标签:单例 clazz1 clone 模式 Clazz Student throws 克隆

3.原型模式

一、引言

在 Java 编程中,原型模式(Prototype)是一种创建对象的方式,通过拷贝原型实例来创建新的对象,为对象的创建提供了一种高效且灵活的途径。本文将详细探讨原型模式的概念、包含的角色、浅克隆与深克隆的实现,以及克隆对单例模式的影响和相应的解决办法。

二、原型模式的定义

原型模式是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。调用者不需要知道任何创建细节,不调用构造函数。

三、原型模式的角色

  1. 抽象原型类:规定了具体原型对象必须实现的 clone() 方法。
  2. 具体原型类:实现抽象原型类的 clone() 方法,是可被复制的对象。
  3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

四、简单的原型模式示例

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Cloneable {
    private String name;
    private String sex;
    private Integer age;

    /**
     * 重写 clone 方法,实现对象的复制
     * @return 复制后的对象
     * @throws CloneNotSupportedException 若不支持克隆则抛出此异常
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    /**
     * 主函数,用于测试原型模式的基本用法
     * @param args 命令行参数
     * @throws Exception 可能抛出的异常
     */
    public static void main(String[] args) throws Exception{
        Student stu1 = new Student("小明", "男", 18);
        // 通过克隆创建 stu2 对象
        Student stu2 = (Student)stu1.clone();
        stu2.setName("小红");
        // 输出 stu1 的信息
        System.out.println(stu1); // Student(name=小明, sex=男, age=18)
        // 输出 stu2 的信息
        System.out.println(stu2); // Student(name=小红, sex=男, age=18)
    }
}

可以看到,把一个学生复制过来,只是改了姓名而已,其他属性完全一样没有改变。需要注意的是,一定要在被拷贝的对象上实现 Cloneable 接口,否则会抛出 CloneNotSupportedException 异常。

四、浅克隆

浅克隆的定义:浅克隆创建一个新对象,新对象的属性和原来对象完全相同,但对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

@Data
public class Clazz implements Cloneable {
    private String name;
    private Student student;

    /**
     * 重写 clone 方法,实现浅克隆
     * @return 浅克隆后的对象
     * @throws CloneNotSupportedException 若不支持克隆则抛出此异常
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
    private String name;
    private String sex;
    private Integer age;
}

/**
 * 主函数,用于测试浅克隆
 * @param args 命令行参数
 * @throws Exception 可能抛出的异常
 */
public static void main(String[] args) throws Exception{
    Clazz clazz1 = new Clazz();
    clazz1.setName("大一");
    Student stu1 = new Student("小明", "男", 18);
    clazz1.setStudent(stu1);
    // 输出 clazz1 的信息
    System.out.println(clazz1); // Clazz(name=大一, student=Student(name=小明, sex=男, age=18))
    Clazz clazz2 = (Clazz)clazz1.clone();
    Student stu2 = clazz2.getStudent();
    stu2.setName("小红");
    // 输出 clazz1 的信息
    System.out.println(clazz1); // Clazz(name=大一, student=Student(name=小红, sex=男, age=18))
    // 输出 clazz2 的信息
    System.out.println(clazz2); // Clazz(name=大一, student=Student(name=小红, sex=男, age=18))
}

可以看到,当修改了 stu2 的姓名时,stu1 的姓名同样也被修改了,这说明 stu1stu2 是同一个对象,这就是浅克隆的特点,对具体原型类中的引用类型的属性进行引用的复制。同时,这也可能是浅克隆所带来的弊端,因为结合该例子的原意,显然是想在班级中新增一名叫小红的学生,而非让所有的学生都改名叫小红,于是我们这里就要使用深克隆。

五、深克隆

深克隆的定义:深克隆创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

@Data
public class Clazz implements Cloneable, Serializable {
    private String name;
    private Student student;

    /**
     * 重写 clone 方法,实现浅克隆
     * @return 浅克隆后的对象
     * @throws CloneNotSupportedException 若不支持克隆则抛出此异常
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    /**
     * 实现深克隆的方法
     * @return 深克隆后的对象
     * @throws IOException 输入输出异常
     * @throws ClassNotFoundException 类未找到异常
     */
    protected Object deepClone() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
}

/**
 * 主函数,用于测试深克隆
 * @param args 命令行参数
 * @throws Exception 可能抛出的异常
 */
public static void main(String[] args) throws Exception{
    Clazz clazz1 = new Clazz();
    clazz1.setName("大一");
    Student stu1 = new Student("小明", "男", 18);
    clazz1.setStudent(stu1);
    Clazz clazz3 = (Clazz)clazz1.deepClone();
    Student stu3 = clazz3.getStudent();
    stu3.setName("王五");
    // 输出 clazz1 的信息
    System.out.println(clazz1); // Clazz(name=大一, student=Student(name=小明, sex=男, age=18))
    // 输出 clazz3 的信息
    System.out.println(clazz3); // Clazz(name=大一, student=Student(name=王五, sex=男, age=18))
}

可以看到,当修改了 stu3 的姓名时,stu1 的姓名并没有被修改了,这说明 stu3stu1 已经是不同的对象了,说明 Clazz 中的 Student 也被克隆了,不再指向原有对象地址,这就是深克隆。这里需要注意的是,Clazz 类和 Student 类都需要实现 Serializable 接口,否则会抛出 NotSerializableException 异常。

六、克隆破坏单例与解决办法

// Clazz 类
@Data
public class Clazz implements Cloneable, Serializable {
    private static Clazz clazz = new Clazz();
    private String name;
    private Student student;

    /**
     * 私有构造函数,防止外部创建实例
     */
    private Clazz(){}

    /**
     * 获取单例实例的方法
     * @return 单例实例
     */
    public static Clazz getInstance() {
        return clazz;
    }

    /**
     * 重写 clone 方法,解决克隆破坏单例的问题
     * @return 单例实例
     * @throws CloneNotSupportedException 若不支持克隆则抛出此异常
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return clazz;
    }
}

/**
 * 测试克隆是否破坏单例
 * @param args 命令行参数
 * @throws Exception 可能抛出的异常
 */
public static void main(String[] args) throws Exception{
    Clazz clazz1 = Clazz.getInstance();
    Clazz clazz2 = (Clazz)clazz1.clone();
    // 输出比较结果
    System.out.println(clazz1 == clazz2); // false

    // 重写 clone 方法后的测试
    Clazz clazz3 = Clazz.getInstance();
    Clazz clazz4 = (Clazz)clazz3.clone();
    System.out.println(clazz3 == clazz4); // true
}

可以看到 clazz1clazz2 并不相等,也就是说他们并不是同一个对象,单例被破坏了。

解决办法:

  1. 不实现 Cloneable 接口即可,但不实现 Cloneable 接口进行 clone 则会抛出 CloneNotSupportedException 异常。
  2. 重写 clone() 方法,返回单例对象。

另外我们知道,单例就是只有一个实例对象,如果重写了 clone() 方法保证单例的话,那么通过克隆出来的对象则不可以重新修改里面的属性,因为修改以后就会连同克隆对象一起被修改,所以是需要单例还是克隆,在实际应用中需要好好衡量。

七、总结

  1. 适用场景:
    • 类初始化消耗资源较多。
    • new 产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。
    • 构造函数比较复杂。
    • 循环体中生产大量对象时。
  2. 优点:
    • 性能优良,Java 自带的原型模式是基于内存二进制流的拷贝,比直接 new 一个对象性能上提升了许多。
    • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建的过程。
  3. 缺点:
    • 必须配备克隆(或者可拷贝)方法。
    • 当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
    • 深克隆、浅克隆需要运用得当。

标签:单例,clazz1,clone,模式,Clazz,Student,throws,克隆
From: https://blog.csdn.net/qq_67028830/article/details/141333015

相关文章

  • JAVA工厂模式
    概要工厂模式提供了一种创建对象的方法,而无需指定要创建的具体类通过使用工厂模式,可以将对象的创建逻辑封装在一个工厂类中,而不是在客户端代码中直接实例化对象,这样可以提高代码的可维护性和可扩展性。类型简单工厂模式:简单工厂模式不是一个正式的设计模式,但它是工厂模式的......
  • 设计模式之cglib动态代理
    什么是动态代理呢?动态代理就是在java进程运行时,通过字节码技术,动态的生成某个类的代理类。在这个代理类中,我们可以做一些额外的操作,一方面仍然保持原有的方法的能力,另外一方面还增强了这些能力。听着是不是AOP有点像,没错,动态代理就是AOP的技术基石。在这之前我曾经写过两篇相关的......
  • 设计模式反模式:UML图示与案例分析
    设计模式反模式:UML图示与案例分析在软件开发中,设计模式是解决问题的有效工具,它们通过提供经过验证的、可复用的解决方案来优化软件设计。然而,当设计模式被误用、滥用或在不适当的情况下应用时,就会形成设计模式反模式(Anti-Patterns)。这些反模式不仅不能解决问题,反而可能引入......
  • 设计模式六大原则 —— 迪米特法则
    设计模式六大原则——迪米特法则在软件设计领域,设计模式六大原则是一组被广泛接受和应用的指导原则,旨在帮助开发者构建更加稳定、灵活、可维护和可扩展的软件系统。这六大原则分别是:单一职责原则(SingleResponsibilityPrinciple,SRP)、开闭原则(Open-ClosedPrinciple,O......
  • 设计模式六大原则中的里氏替换原则
    设计模式六大原则中的里氏替换原则(LiskovSubstitutionPrinciple,LSP)是面向对象设计中一个至关重要的原则,它定义了继承的基本原则和约束,确保子类能够透明地替换父类,而不会破坏系统的正确性和稳定性。以下是对里氏替换原则的详细阐述,包括其定义、应用、重要性、以及在实际......
  • 【WCH蓝牙系列芯片】-基于CH582开发板—蓝牙从机HAL_SLEEP模式,串口唤醒收发数据
    -------------------------------------------------------------------------------------------------------------------------------------在之前的博客文档中介绍过CH582作为蓝牙主机,开启睡眠后,通过串口唤醒,并接收串口数据。这里再讲解一下使用CH582芯片作为蓝牙从机,开......
  • Java 设计模式
    23种设计模式创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。结构型模式:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模......
  • Java单例模式
    定义单例模式(SingletonPattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这种模式的核心在于控制类的实例化过程,保证在任何时间点,一个类只有一个实例存在,并且这个实例可以被系统的所有其他部分通过一个公共的访问点访问。1、唯一实例:单......
  • 访问者模式
    在访问者模式(VisitorPattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。双龙物流介绍意图旨......
  • 基于STM32F407ZGT6芯片的GPIO工作模式
    目录4种输入模式4种输出模式输入模式模拟输入浮空输入上拉输入下拉输入输出模式推挽输出开漏输出复用推挽输出复用开漏输出4种输入模式(1)GPIO_Mode_IN_FLOATING浮空输入(2)GPIO_Mode_IPU上拉输入(3)GPIO_Mode_IPD下拉输入(4)GPIO_Mode_AIN模拟输入4种输出模......