• 一文学会如何使用原型模式


    原型模式是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。

    原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。当对象的构建过程比较耗时时,可以利用系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。我们先来看一个简单的案例结构图。

    在这里插入图片描述

    UML图中,我们可以看出主要包含三个角色:

    • 客户端

      客户端提出创建的请求

    • 抽象原型

      规定拷贝接口

    • 具体原型

      被拷贝的对象

    应用场景

    • 类初始化消耗资源较多
    • new 产生的一个对象需要非常繁琐的过程(数据准备,访问权限等)
    • 构造函数比较复杂
    • 循环体中生产大量对象时

    原型模式的通用写法

    按照如上类图,一个标准的原型模式代码应该具有三部分。首先我们创建原型IPrototype接口:

    public interface IPrototype<T> {
    
        T clone();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    创建具体的需要克隆的对象ConcretePrototype

    public class ConcretePrototype  implements IPrototype{
    
        private int age;
    
        private String name;
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public ConcretePrototype clone() {
            ConcretePrototype current = new ConcretePrototype();
            current.setAge(this.age);
            current.setName(this.name);
            return current;
        }
    
        @Override
        public String toString() {
            return "ConcretePrototype{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    测试代码:

    public class Main {
    
        public static void main(String[] args) {
            // 创建原型对象
            ConcretePrototype prototype = new ConcretePrototype();
            prototype.setName("CC");
            prototype.setAge(18);
            System.out.println(prototype);
    
            // 拷贝
            ConcretePrototype clone = prototype.clone();
            System.out.println(clone);
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    以上便是一个最简单的原型模式。虽然在这个简单场景下,我们这样操作好像复杂了,但是如果有几百个属性需要复制,采用这个方法就比较方便了。但是,上面的复制过程是我们自己完成的,在实际编码中我们一般不会做这种体力劳动,一般我们会采用浅克隆,深克隆两种方法。

    浅克隆

    普通写法中,我们是自己定义的一个克隆接口,实际上JDK已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。

    我们来改造下ConcretePrototype类的代码:

    public class ConcretePrototype  implements Cloneable {
    
        private int age;
    
        private String name;
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public ConcretePrototype clone() {
            try {
                return (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public String toString() {
            return "ConcretePrototype{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    这个时候重新测试,可以得到同样的结果。通过这个方式再多的属性赋值我们也能轻而易举的搞定了。

    但是这种方法也会有点问题,我们再来测试一下,首先我们在原型类中添加要给爱好属性:

    @Data
    public class ConcretePrototype  implements Cloneable {
    
        private int age;
    
        private String name;
    
        private List<String> hobbies;
    
        @Override
        public ConcretePrototype clone() {
            try {
                return (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    修改客户端测试代码:

    public class Main {
    
        public static void main(String[] args) {
            // 创建原型对象
            ConcretePrototype prototype = new ConcretePrototype();
            prototype.setName("CC");
            prototype.setAge(18);
            List<String> hobbies = new ArrayList<>();
            hobbies.add("书法");
            hobbies.add("音乐");
            prototype.setHobbies(hobbies);
            System.out.println(prototype);
    
            // 拷贝
            ConcretePrototype clone = prototype.clone();
            clone.getHobbies().add("游戏");
            System.out.println(clone); //克隆对象
            System.out.println(prototype); //原型对象
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    ConcretePrototype(age=18, name=CC, hobbies=[书法, 音乐])
    ConcretePrototype(age=18, name=CC, hobbies=[书法, 音乐, 游戏])
    ConcretePrototype(age=18, name=CC, hobbies=[书法, 音乐, 游戏])
    
    • 1
    • 2
    • 3

    我们给克隆对象添加了一个爱好后,发现原型对象也发生了变化,这显然不符合我们的预期。因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。从测试结果分析来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话如果我们修改任意一个对象的属性值,两个对象的值都会变化。这就是我们常说的浅克隆:只是完整复制了值类型数据,没有复制引用对象。这显然不是我们想要的结果,为了处理这个问题,我们需要使用到深克隆。

    深克隆

    深克隆我们这里主要使用序列化以及反序列化来实现,当然也可以使用其他方法,比如json的方式,通过json字符串将其转换为对象实习。

    我们继续改造代码,增加一个deepClone方法。

    由于使用序列化,所以我们需要实现Serializable接口。

    @Data
    public class ConcretePrototype  implements Cloneable,Serializable {
    
        private int age;
    
        private String name;
    
        private List<String> hobbies;
    
        @Override
        public ConcretePrototype clone() {
            try {
                return (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public ConcretePrototype deepClone() {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(this);
    
                ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(bis);
    
                return (ConcretePrototype) ois.readObject();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    这个时候我们再去测试,发现结果是我们想要的了,两个对象互不影响。

    如何解决克隆破坏单例模式

    如果我们克隆的对象是单例对象,那意味着克隆就会破坏单例。实际上防止克隆破坏单例模式的解决思路非常简单,禁止克隆即可。要么我们直接单例类不实现Cloneable接口;要么我们重写clone方法,在clone方法中直接返回单例对象。

    @Data
    public class ConcretePrototype  implements Cloneable {
    
        private int age;
    
        private String name;
    
        private List<String> hobbies;
    
        private static ConcretePrototype instance = new ConcretePrototype();
    
        private static ConcretePrototype getInstance(){
            return instance;
        }
    
        @Override
        public ConcretePrototype clone() {
            return instance;
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    优缺点

    优点

    • 性能优良,基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
    • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并保存起来,简化了创建对象的过程,以便在需要的时候使用,类似撤销操作

    缺点

    • 需要为每个类配置一个克隆方法
    • 克隆方法位于类的内部,当对已有类改造的时候需要又改代码,破坏了开闭原则。
    • 在实现深克隆时需要编写较复杂的代码,而且当对象之前存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来比较麻烦。
  • 相关阅读:
    Java校招120道面试题目合集
    Vue 官方文档2.x教程学习笔记 1 基础 1.3 Vue 实例 1.3.1 创建一个Vue 实例 & 1.3.2 数据与方法
    【广州华锐互动】VR溺水预防教育:在虚拟世界中学会自救!
    继承和动态内存分配
    RabbitMQ消息中间件
    论文分享|NeurIPS2022‘华盛顿大学|俄罗斯套娃表示学习(OpenAI使用的文本表示学习技术)
    Win10下Qt配置opencv/libtorch闭坑总结
    MySQL高级篇——事务
    Ionic list - ion-item的相关用法
    Stable Diffusion WebUI扩展sd-webui-controlnet之Canny
  • 原文地址:https://blog.csdn.net/qq_41432730/article/details/126455931