• 原型模式-Prototype Pattern


    原文地址:https://jaune162.blog/design-pattern/prototype-pattern/
    全系列免费设计模式课程持续更新中:https://books.jaune162.blog/docs/design-pattern/Intoduction

    引言

    在Java中如果我们想要拷贝一个对象应该怎么做?第一种方法是使用 gettersetter方法一个字段一个字段设置。或者使用 BeanUtils.copyProperties() 方法。这种方式不仅能实现相同类型之间对象的拷贝,还可以实现不同类型之间的拷贝。

    如果仅考虑相同对象之间的拷贝,有没有什么更优雅的方式呢?那就是原型模式。

    定义及实现

    定义

    Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

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

    结构

    原型模式就是类中提供一个拷贝方法,用于拷贝一个和自身属性一模一样的对象。

    代码实现

    第一种方式

    public interface Prototype<T> {
        T copy();
    }
    
    @NoArgsConstructor
    @Data
    public class ConcretePrototype1 implements Prototype<ConcretePrototype1> {
    
        private String name;
    
        private Integer age;
    
        public ConcretePrototype1(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public ConcretePrototype1 copy() {
            return new ConcretePrototype1(this.name, this.age);
        }
    }
    
    public class Main {
    
        public static void main(String[] args) {
            ConcretePrototype1 p1 = new ConcretePrototype1();
            p1.setAge(18);
            p1.setName("prototype1");
            System.out.println(p1);
    
            ConcretePrototype1 p2 = p1.copy();
            System.out.println(p2);
        }
    }
    
    • 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

    第二种方式

    只需要类实现 java.lang.Cloneable 接口,并实现clone() 方法即可。

    @NoArgsConstructor
    @Data
    public class ConcretePrototype1 implements Cloneable {
    
        private String name;
    
        private Integer age;
    
        @Override
        public ConcretePrototype1 clone() {
            try {
                return (ConcretePrototype1) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new AssertionError();
            }
        }
    }
    
    public class Main {
    
        public static void main(String[] args) {
            ConcretePrototype1 p1 = new ConcretePrototype1();
            p1.setAge(18);
            p1.setName("prototype1");
            System.out.println(p1);
    
            ConcretePrototype1 p2 = p1.clone();
            System.out.println(p2);
        }
    }
    
    • 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

    以上方法的问题

    以上方法的问题在于,如果有对象类型的数据。会直接引用对象地址,对象的内容修改后会同时影响拷贝对象和被拷贝对象,如下:

    @NoArgsConstructor
    @Data
    public class Address {
    
        private String province;
    
        private String city;
    
        private String street;
    
        public Address(String province, String city, String street) {
            this.province = province;
            this.city = city;
            this.street = street;
        }
    }
    
    @NoArgsConstructor
    @Data
    public class ConcretePrototype1 implements Cloneable {
    
        private String name;
    
        private Integer age;
    
        private Address address;
    
        @Override
        public ConcretePrototype1 clone() {
            try {
                return (ConcretePrototype1) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new AssertionError();
            }
        }
    }
    
    public class Main {
    
        public static void main(String[] args) {
            Address address = new Address("河南省", "郑州市", "高新区");
    
            ConcretePrototype1 p1 = new ConcretePrototype1();
            p1.setAge(18);
            p1.setName("prototype1");
            p1.setAddress(address);
            System.out.println(p1);
    
            ConcretePrototype1 p2 = p1.clone();
            System.out.println(p2);
    
            // 修改p1的地址信息
            p1.getAddress().setStreet("中原区");
    
            System.out.println(p1);
            System.out.println(p2);
    
            // 修改p2的地址信息
            p2.getAddress().setStreet("二七区");
            System.out.println(p1);
            System.out.println(p2);
        }
    }
    
    • 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
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    输出

    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=中原区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=中原区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=二七区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=二七区))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从输出的结构中可以看出,无论是 p1 对象修改了 address 对象的内容,还是p2 对象修改了 address 对象的内容,两者都会改变。这是因为p1p2都指向了同一个 address 对象。

    String 类型和 Integer 类型也是对象类型,为什么给name重新赋值时,p1p2不会相互影响呢?下面我们来解答这个问题。

    这个问题其实很好回答,p1p2name字段在拷贝完成后其实指向的是同一个对象。从断点就可以看出。

    但是我们重新给p1name 赋值时,相当于将p1name指向了另一个字符串对象。如下图

    希望不要在这个地方有疑惑。

    我们回到正题,现在我们想让两个拷贝的对象,拷贝完成后就不再相互影响,怎么办?

    那就是用序列化和反序列化的方式来实现对象的深拷贝。

    深拷贝

    深拷贝就是将对象序列化,然后再反序列化。这样新创建的对象跟原对象没有任何关系。任何字段都不会同时指向同一个对象。序列化方式主要有JSON序列化、Java原生序列化方式,当然还有其他的序列化方式。这里只列举JSON序列化方式,其他序列化如果有兴趣可以自行实现。

    一些第三方库如Apache Commons的SerializationUtils类或Google的Gson库都提供了实现深拷贝的方法。

    JSON序列化方式

    在本例中使用 fastjson2 进行序列化和反序列化。

    @NoArgsConstructor
    @Data
    public class ConcretePrototype1 implements Prototype<ConcretePrototype1> {
    
        private String name;
    
        private Integer age;
    
        private Address address;
    
        @Override
        public ConcretePrototype1 copy() {
            String json = JSON.toJSONString(this);
            return JSON.parseObject(json, ConcretePrototype1.class);
        }
    }
    
    public class Main {
    
        public static void main(String[] args) {
            Address address = new Address("河南省", "郑州市", "高新区");
    
            ConcretePrototype1 p1 = new ConcretePrototype1();
            p1.setAge(18);
            p1.setName("prototype1");
            p1.setAddress(address);
            System.out.println(p1);
    
            ConcretePrototype1 p2 = p1.copy();
            System.out.println(p2);
    
            p1.getAddress().setStreet("中原区");
            System.out.println(p1);
            System.out.println(p2);
        }
    }
    
    • 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

    输出结果:

    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=中原区))
    ConcretePrototype1(name=prototype1, age=18, address=Address(province=河南省, city=郑州市, street=高新区))
    
    • 1
    • 2
    • 3
    • 4

    从输出结果中可以看出,两个对象是不会相互影响的。

    从上图的调试结果中也可以看出,所有字段指向的内存地址都不一样。

    实际应用

    原型模式的一个典型应用就是Spring中Bean的作用域。Spring框架中的原型作用域(Prototype Scope)就是基于原型模式实现的。

    在Spring框架中,当一个bean的作用域被定义为原型作用域时,Spring容器在接收到对该bean的请求时,会为每个请求创建一个新的实例。这就类似于原型模式中的克隆操作,每次都创建一个新的对象实例,而不是返回同一个实例。

    但是在Spring中Bean的创建是通过BeanDefinition创建的。BeanDefinition是Spring框架中用于描述和定义Bean的元数据接口。它包含了Bean的类名、依赖、作用域、生命周期回调等信息,可以理解为Bean的配置信息。

    当Spring容器启动时,它会解析配置文件或注解,将Bean定义解析为BeanDefinition,并将其注册到容器中。然后,Spring容器根据BeanDefinition中的信息来创建和管理Bean的实例。

    也就是说BeanDefinition是对象的模版,当需要创建对象是,通过这个模板来创建一个新的。这与本篇文章中的原型模式有些区别。

    总结

    • 原型模式就是通过一个以存在的对象来创建另一个。
    • 如果对象中存在有字段是对象类型,当这个字段被修改后,会同时影响拷贝和被拷贝对象。这时需要用深拷贝来处理。
  • 相关阅读:
    红海竞争下,「社交+」在中东泛娱乐 App 市场的出海新机遇
    springboot整合canal
    【chat】2:vs2022 连接远程ubuntu服务器远程cmake开发
    vite 和 webpack 的区别
    C++并发线程之 互斥量、lock_guard类模板、死锁
    如何设计分布式系统-分布式事务-XA?
    vue项目中使用echarts
    Go程序内存泄露问题快速定位
    日常电脑出现msvcp140.dll丢失的解决办法
    项目平台——项目首页设计(五)
  • 原文地址:https://blog.csdn.net/jaune161/article/details/136133513