原型模式

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

一、定义

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

原型模式的核心在于拷贝原型对象,以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升了许多,当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对基进行克隆(一般是基于二进制流的复制)。

原型模式的重要三大角色:

客户(Client):客户类提出创建对象的请求

抽象原型(Prototype):规定拷贝接口

具体原型(Concrete Prototype):被拷贝的对象。

二、应用场景

大家一定经历过大篇幅的创建Geter,Setter赋值的场景,很多人可能也习惯在赋值进行大量的GIE/SET并没觉得有什么不对,但我觉得这样代码属于纯体力活,就像前面我说反射一样,JSON字符串用反射来做三行代码搞定,这都是一个码农走向架构过程中思想转变的过程,而设计模式就是一个很好的思想借用方式,如果我们在GET/SET场景中用到了原型模式,那可以帮我们避免大量的纯体力劳动,提高开发效率。

原型模式主要适用于以下场景:
1、类初始化消耗资源较多。
2、new 产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
3、构造函数比较复杂。
4、循环体中生产大量对象时。
在 Spring 中,原型模式应用得非常广泛。例如 scope=“prototype”,在我们经常用的 JSON.parseObject()也是一种原型模式

 三、浅克隆

/**
 * 抽象原型(Prototype):规定拷贝接口
 */
public interface Prototype<T> {
    T clone();
}

 

/**                                                                                      
 * 具体原型(Concrete Prototype):被拷贝的对象。                                                      
 */                                                                                      
@Data                                                                                    
public class ConcretePrototypeA implements Prototype<ConcretePrototypeA>{                
    private int age;                                                                     
    private String name;                                                                 
    private List hobbies;                                                                
                                                                                         
    public ConcretePrototypeA(int age,String name,List hobbies){                         
        this.age=age;                                                                    
        this.name=name;                                                                  
        this.hobbies=hobbies;                                                            
    }                                                                                    
                                                                                         
                                                                                         
    @Override                                                                            
    public ConcretePrototypeA clone() {                                                  
        return new ConcretePrototypeA(this.age,this.name,this.hobbies);                  
    }                                                                                    
                                                                                         
    @Override                                                                            
    public String toString() {                                                           
        return "ConcretePrototypeA{" +                                                   
                "age=" + age +                                                           
                ", name='" + name + '\'' +                                               
                ", hobbies=" + hobbies +                                                 
                '}';                                                                     
    }                                                                                    
}                                                                                        
                                                                                         

 

/**
 * 具体原型(Concrete Prototype):被拷贝的对象。
 */
@Data
public class ConcretePrototypeB implements Prototype<ConcretePrototypeB>{
    private int age;
    private String name;
    private List hobbies;

    public ConcretePrototypeB(int age, String name, List hobbies){
        this.age=age;
        this.name=name;
        this.hobbies=hobbies;
    }


    @Override
    public ConcretePrototypeB clone() {
        return new ConcretePrototypeB(this.age,this.name,this.hobbies);
    }

    @Override
    public String toString() {
        return "ConcretePrototypeA{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

 

/**
 * 客户(Client):客户类提出创建对象的请求
 */
public class Client {
    private Prototype prototype;
    public Client(Prototype prototype){ this.prototype = prototype; }
    public Prototype startClone(Prototype prototype){
        return (Prototype)prototype.clone();
    }
}

 

public class PrototypeTest {
    public static void main(String[] args) {
        List hobbies = new ArrayList<String>();

        // 创建一个具体的需要克隆的对象
        ConcretePrototypeA concretePrototype = new ConcretePrototypeA(18, "prototype", hobbies);
        // 填充属性,方便测试
        System.out.println(concretePrototype);

        // 创建 Client 对象,准备开始克隆
        Client client = new Client(concretePrototype);
        ConcretePrototypeA concretePrototypeClone = (ConcretePrototypeA) client.startClone(concretePrototype);
        System.out.println(concretePrototypeClone);
        System.out.println(concretePrototypeClone==concretePrototype);
        System.out.println("克隆对象中的引用类型地址值:" + concretePrototypeClone.getHobbies());
        System.out.println("原对象中的引用类型地址值:" + concretePrototype.getHobbies());
        System.out.println("对象地址比较:"+(concretePrototypeClone.getHobbies() == concretePrototype.getHobbies()));
    }
}

 

 

 从测试结果看出 hobbies 的引用地址是相同的,意味着复制的不是值,而是引用的地址。这样的话 ,如果我们修改任意一个对象中的属性值,concretePrototype 和concretePrototypeCone 的 hobbies 值都会改变。这就是我们常说的浅克隆。只是完整复制了值类型数据,没有赋值引用对象。换言之,所有的引用对象仍然指向原来的对象

四、分析JDK浅克隆API带来的问题

在java提供的API中,不需要手动创建抽象原型接口,java内置了Cloneable抽象原型接口,自定义的类型只需要实现该接口并重写Object.clone方法即可完成对本类的复制。

通过查看JDK的源码发现,其实Cloneable是一个空接口。java之所以提供Cloneable接口,只是为了在运行时通知java虚拟机可以安全的在该类上使用clone方法。如果没有实现Cloneable接口,调用clone()方法会抛出CloneNotSupportedException异常。

如果使用clone()方法,需要满足以下条件:

对于任何对象o,都有o.clone() != o,也就是说克隆产生的对象和原对象不是同一个对象。

对于任何对象o,都有o.clone().getClass() == o.getClass(),也就是说克隆产生的对象和原对象类型相同。

对于任何对象o,应该有o.clone().equals(o)成立,为什么这样不是绝对的呢,因为如果在不重写hashcode和equals的情况下,是不能保证两个对象equals相等的。
对上面代码改造

/**                                                                                  
 * 具体原型(Concrete Prototype):被拷贝的对象。                                                  
 */                                                                                  
@Data                                                                                
public class ConcretePrototypeA implements Cloneable{                                
    private int age;                                                                 
    private String name;                                                             
    private List<String> hobbies;                                                    
                                                                                     
                                                                                     
    @Override                                                                        
    public ConcretePrototypeA clone() {                                              
        try {                                                                        
            return (ConcretePrototypeA) super.clone();                               
        } catch (CloneNotSupportedException e) {                                     
            e.printStackTrace();                                                     
        }                                                                            
        return null;                                                                 
    }                                                                                
                                                                                     
    @Override                                                                        
    public String toString() {                                                       
        return "ConcretePrototypeA{" +                                               
                "age=" + age +                                                       
                ", name='" + name + '\'' +                                           
                ", hobbies=" + hobbies +                                             
                '}';                                                                 
    }                                                                                
}                                                                                    
                                                                                     
public class PrototypeTest {
    public static void main(String[] args) {
//        List hobbies = new ArrayList<String>();
//
//        // 创建一个具体的需要克隆的对象
//        ConcretePrototypeA concretePrototype = new ConcretePrototypeA(18, "prototype", hobbies);
//        // 填充属性,方便测试
//        System.out.println(concretePrototype);
//
//        // 创建 Client 对象,准备开始克隆
//        Client client = new Client(concretePrototype);
//        ConcretePrototypeA concretePrototypeClone = (ConcretePrototypeA) client.startClone(concretePrototype);
//        System.out.println(concretePrototypeClone);
//        System.out.println(concretePrototypeClone==concretePrototype);
//        System.out.println("克隆对象中的引用类型地址值:" + concretePrototypeClone.getHobbies());
//        System.out.println("原对象中的引用类型地址值:" + concretePrototype.getHobbies());
//        System.out.println("对象地址比较:"+(concretePrototypeClone.getHobbies() == concretePrototype.getHobbies()));
        ConcretePrototypeA concretePrototype = new ConcretePrototypeA();
        concretePrototype.setAge(11);
        concretePrototype.setName("张三");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("没变化");
        concretePrototype.setHobbies(hobbies);
        // 创建一个具体的需要克隆的对象

        // 创建 Client 对象,准备开始克隆
        ConcretePrototypeA concretePrototypeClone = concretePrototype.clone();
        // 填充属性,方便测试
        concretePrototypeClone.getHobbies().add("有变化");
        System.out.println(concretePrototype);
        System.out.println(concretePrototypeClone);

    }
}

可以看到,我对克隆之后的值进行修改,基础数据类型改变了,没有影响原对象,但是改变引用类型List,则原对象的值也发生了改变。这就叫浅拷贝,这也是浅克隆最致命的问题。那么怎么解决这个问题呢,使用深克隆可以解决该问题。

 

五、使用序列化实现深克隆

 

/**
 * 深拷贝,增加一个deepClone()方法
 */
@Data
public class ConcretePrototypeA implements Cloneable, Serializable {
    private int age;
    private String name;
    private List<String> hobbies;


    @Override
    public ConcretePrototypeA clone() {
        try {
            return (ConcretePrototypeA) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    public ConcretePrototypeA 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 (ConcretePrototypeA) ois.readObject();
        }catch (Exception e){
             e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        return "ConcretePrototypeA{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

 

public class ConcretaPrototypeTest {
    public static void main(String[] args) {
        ConcretePrototypeA concretePrototypeA=new ConcretePrototypeA();
        concretePrototypeA.setAge(11);
        concretePrototypeA.setName("张三");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("没变化");
        concretePrototypeA.setHobbies(hobbies);
        // 创建一个具体的需要克隆的对象

        // 创建 Client 对象,准备开始克隆
        ConcretePrototypeA concretePrototypeClone = concretePrototypeA.deepClone();
        // 填充属性,方便测试
        concretePrototypeClone.getHobbies().add("有变化");
        System.out.println(concretePrototypeA==concretePrototypeClone);
        System.out.println(concretePrototypeA);
        System.out.println(concretePrototypeClone);
        System.out.println(concretePrototypeA.getHobbies()==concretePrototypeClone.getHobbies());
    }
}

 

 

 

这次修改克隆对象没有影响到原对象,达到了我们的目的。

 

 六、原型破坏单例

如果我们克隆的目标的对象是单例对象,那意味着,深克隆就会破坏单例。实际上防止克隆破坏单例解决思路非常简单,禁止深克隆便可。要么我们的单例类不实现Cloneable 接口;要么我们重写 clone()方法,在 clone 方法中返回单例对象即可
破坏单例示例:
/**
 * 破坏单例示例
 */
public class ConcretePrototype implements Cloneable{
    private static ConcretePrototype concretePrototype=new ConcretePrototype();
    private ConcretePrototype(){}

    public static ConcretePrototype getInstance(){
        return concretePrototype;
    }
    public ConcretePrototype clone(){
        try {
            return (ConcretePrototype) super.clone();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}
public class ConcretePrototypeTest {
    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype concretePrototype=ConcretePrototype.getInstance();

        //复制原型对象
        ConcretePrototype cloneType=concretePrototype.clone();
        System.out.println(concretePrototype==cloneType);
    }
}

 

 看上面结果是把单例破坏了,下面来改动下保证单例

    public ConcretePrototype clone(){
         
        return concretePrototype;
    }

七、Cloneable 源码分析

先看我们常用的 ArrayList 就实现了 Cloneable 接口,来看代码 clone()方法的实现:
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

1、调用父类的克隆方法可以一个ArrayList出来我查看源码没有父类中发现clone方法, 也就是说调用的是Object的clone方法。 那么只是简单的浅拷贝。 既然是浅拷贝说明详见的list和原来的list中的elementData 其实是同一个数组

2、调用Arrays.copyOf方法复制一份新的数组, 赋值给新的List.这个copyOf方法只是复制数组中的引用, 说明两个数组虽然分开了, 但是内部的存储的对象还是一样的。

克隆的方法克隆了新的List , 但是两个list中装载的对象还是同一份。

验证代码:

public class A implements Serializable{

    int x = 0;

    public static void main(String[] args) throws  IOException, ClassNotFoundException {
        ArrayList<A> list = new ArrayList<>();
        list.add(new A());

        ArrayList<A> cloneList = (ArrayList<A>) list.clone();

        System.out.println("比较克隆前后的对象是否同一个");
        System.out.println(list.get(0) == cloneList.get(0));

    }
}

八、原型模式优点

1、java自带的原型模式基于内存二进制流的复制,在性能上比直接new一个对象更加好

2、可以使用深克隆方式保存对象状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

九、原型模式缺点

1、需要为每一个类都配置一个clone方法

2、clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。

3、当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦,因此,深克隆、㳀克隆需要运用得当。

 

git源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/design_pattern.git


 

 

posted @ 2021-04-01 09:27  童话述说我的结局  阅读(116)  评论(0编辑  收藏  举报