java设计模式之原型模式

Posted J_Newbie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java设计模式之原型模式相关的知识,希望对你有一定的参考价值。

定义:

原型模式指原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象,属于创建型设计模式

原型模式的核心在于复制原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行复制,不需要在经历耗时的对象初始化过程(不调用构造函数),性能提升许多。当对象的构建过程比较耗时,可以把当前系统中已存在的对象作为原型,对其进行复制(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大缩短。

应用场景:

  1. 创建对象成本较大(例如:初始化时间长,占用CPU太多,或者占用网络资源太多等),需要优化资源

  2. 创建一个对象需要繁琐的数据准备或者访问权限等,需要提高性能或者提高安全性

  3. 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值

在Spring中,原型模式应用非常广泛,例如scope=prototype,JSON.parseObject(),都是原型模式的具体应用

UML类图:

 

由上图可以看到,原型模式主要包含3个角色

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

  2. 抽象原型(IPrototype):规定复制接口

  3. 具体原型(ConcretePrototype):被复制的对象

注:不是通过new关键字而是通过对象复制来实现创建对象的模式被称为原型模式

通用写法

//客户端
package com.design.pattern.Prototype;

public class Client 
    public static void main(String[] args) 
        //创建原型对象
        ConcretePrototypeA prototypeA = new ConcretePrototypeA("originalA");
        System.out.println(prototypeA);
        //复制原型对象
        ConcretePrototypeA cloneTypeA = prototypeA.clone();
        System.out.println(cloneTypeA);
        cloneTypeA.desc="clone";
        System.out.println(cloneTypeA);
    

//抽象原型
package com.design.pattern.Prototype;

public interface IPrototype<T> 
    T clone();

//具体原型
package com.design.pattern.Prototype;

public class ConcretePrototypeA implements IPrototype<ConcretePrototypeA>
    public String desc;

    public ConcretePrototypeA(String desc) 
        this.desc = desc;
    

    @Override
    public ConcretePrototypeA clone() 
        return new ConcretePrototypeA(this.desc);
    

    @Override
    public String toString() 
        return "ConcretePrototypeA" +
                "desc='" + desc + '\\'' +
                '';
    

//具体原型
package com.design.pattern.Prototype;

public class ConcretePrototypeB implements IPrototype<ConcretePrototypeB>
    private String desc;

    public ConcretePrototypeB(String desc) 
        this.desc = desc;
    

    @Override
    public ConcretePrototypeB clone() 
        return new ConcretePrototypeB(this.desc);
    

    @Override
    public String toString() 
        return "ConcretePrototypeB" +
                "desc='" + desc + '\\'' +
                '';
    

使用原型模式解决实际问题

分析jdk浅克隆API带来的问题:

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

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

一般情况下,如果使用clone()方法,则需满足以下条件

  1. 对任何对象o,都有o.clone() != o,换言之,克隆对象与原型对象不是同一个对象

  2. 对任何对象o,都有o.clone(),getClass() = o.getClass(),换言之,克隆对象与原型对象的类型一样

  3. 如果对象o的equals()方法定义恰当,则o.clone().equals(o)应当成立

我们在设计自定义类的clone()方法时,应当遵守这3个条件,一般来说,这3个条件的前2个是必须的第3个是可选的。

下面使用java提供的api应用来实现原型模式,代码如下

package com.design.pattern.Prototype;

public class ClientJDK 
    public static void main(String[] args) 
        //创建原型对象
        ConcretePrototype prototype = new ConcretePrototype("original");
        System.out.println(prototype);
        //复制原型对象
        ConcretePrototype clone = prototype.clone();
        System.out.println(clone);
        System.out.println(prototype == clone);
        System.out.println(prototype.getClass() == clone.getClass());
        clone.desc = "clone";
        System.out.println(clone);
    

package com.design.pattern.Prototype;

public class ConcretePrototype implements Cloneable

    public String desc;

    public ConcretePrototype(String desc) 
        this.desc = desc;
    
    @Override
    public ConcretePrototype clone() 
        ConcretePrototype cloneType = null;
        try 
            cloneType = (ConcretePrototype) super.clone();
        catch (Exception e)
            e.printStackTrace();
        
        return cloneType;
    

    @Override
    public String toString() 
        return "ConcretePrototype" +
                "desc='" + desc + '\\'' +
                '';
    

Super.clone()方法直接从对内存中以二进制流的方式进行复制,重新分配一个内存块,因此效率很高,由于super.clone()方法基于内存复制,因此不会调用对象的构造函数,也就是不需要经历初始化的过程

在日常开发中,使用super.clone()方法并不能满足所有需求。如果类中存在引用对象属性,则原型对象与克隆对象的该属性会指向同一对象的引用

package com.design.pattern.Prototype;

import java.util.ArrayList;
import java.util.List;

public class ClientJDK 
    public static void main(String[] args) 
        //创建原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("张三");
        List<String> hobbies = new ArrayList<>();
        hobbies.add("书法");
        hobbies.add("美术");
        prototype.setHobbies(hobbies);
        System.out.println(prototype);

        ConcretePrototype clone = prototype.clone();
        clone.getHobbies().add("java");
        System.out.println("原型对象: " + prototype);
        System.out.println("克隆对象:" + clone);
    

package com.design.pattern.Prototype;

import lombok.Data;

import java.util.List;
@Data
public class ConcretePrototype implements Cloneable

    public int age;
    public String name;
    public List<String> hobbies;

    @Override
    public ConcretePrototype clone() 
        ConcretePrototype cloneType = null;
        try 
            cloneType = (ConcretePrototype) super.clone();
        catch (Exception e)
            e.printStackTrace();
        
        return cloneType;
    

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

运行结果:

ConcretePrototypeage=18, name='张三', hobbies=[书法, 美术]
原型对象: ConcretePrototypeage=18, name='张三', hobbies=[书法, 美术, java]
克隆对象:ConcretePrototypeage=18, name='张三', hobbies=[书法, 美术, java]

我们给克隆对象新增了一个属性hobbies(爱好)之后,发现原型对象也发生了变化,这显然不符合预期。因为我们希望克隆对象和原型对象是两个独立的对象。从测试结果来看,应该是hobbies共用一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值,原型对象与克隆对象的值都会改变。这就是我们常说的浅克隆,只是完整的复制了值的类型数据,没有赋值引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。

java自带的clone()方法进行的就是浅克隆。而如果我们想进行深克隆,可以直接在super.clone()后,手动给克隆对象的相关属性分配另一块内存,不过如果当原型对象维护很多引用属性的时候,手动分配比较麻烦。因此,java中,如果想完成原型对象的深克隆,则通常使用序列化的方式

使用序列化实现深克隆:

对上面的代码,我们增加一个deepClone()方法

package com.design.pattern.Prototype;

import lombok.Data;

import java.io.*;
import java.util.List;
@Data
public class ConcretePrototype implements Cloneable, Serializable 

    public int age;
    public String name;
    public List<String> hobbies;

    @Override
    public ConcretePrototype clone() 
        ConcretePrototype cloneType = null;
        try 
            cloneType = (ConcretePrototype) super.clone();
        catch (Exception e)
            e.printStackTrace();
        
        return cloneType;
    

    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;
        
    
    @Override
    public String toString() 
        return "ConcretePrototype" +
                "age=" + age +
                ", name='" + name + '\\'' +
                ", hobbies=" + hobbies +
                '';
    


package com.design.pattern.Prototype;

import java.util.ArrayList;
import java.util.List;

public class ClientJDK 
    public static void main(String[] args) 
        //创建原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("张三");
        List<String> hobbies = new ArrayList<>();
        hobbies.add("书法");
        hobbies.add("美术");
        prototype.setHobbies(hobbies);
        System.out.println(prototype);

        ConcretePrototype clone = prototype.deepClone();
        clone.getHobbies().add("java");
        System.out.println("原型对象: " + prototype);
        System.out.println("克隆对象:" + clone);
        System.out.println(prototype == clone);
        System.out.println("原型对象的爱好:" + prototype.getHobbies());
        System.out.println("克隆对象的爱好:" + clone.getHobbies());
        System.out.println(prototype.getHobbies() == clone.getHobbies());
    

运行结果:

ConcretePrototypeage=18, name='张三', hobbies=[书法, 美术]
原型对象: ConcretePrototypeage=18, name='张三', hobbies=[书法, 美术]
克隆对象:ConcretePrototypeage=18, name='张三', hobbies=[书法, 美术, java]
false
原型对象的爱好:[书法, 美术]
克隆对象的爱好:[书法, 美术, java]
false

还原克隆破坏单例的事故现场

假设如果复制的目标对象恰好是单例对象,我们试一下,

package com.design.pattern.Prototype;

public class SingletonPrototype implements Cloneable

    private static SingletonPrototype instance = new SingletonPrototype();
    private SingletonPrototype();
    public static SingletonPrototype getInstance()
        return instance;
    
    @Override
    public SingletonPrototype clone()
        try
            return (SingletonPrototype) super.clone();
        catch (Exception e)
            e.printStackTrace();
            return null;
        
    


package com.design.pattern.Prototype;

public class ClientTest 
    public static void main(String[] args) 
        SingletonPrototype instance = SingletonPrototype.getInstance();
        SingletonPrototype clone = instance.clone();
        System.out.println(instance == clone);
    

运行结果:

false

从运行结果来看,确实创建了两个不同的对象,实际上防止复制破坏单例对象的解决思路非常简单,禁止复制便可。要么我们单例类不实现Cloneable接口,要么我们重写clone()方法,在clone()方法中返回单例对象即可,具体代码如下

@Override
    public SingletonPrototype clone()
        return instnce;
    

优点:

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

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

缺点:

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

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

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

以上是关于java设计模式之原型模式的主要内容,如果未能解决你的问题,请参考以下文章

JAVA设计模式之原型模式

java设计模式之原型模式

java设计模式之原型模式

java设计模式之原型模式

JAVA设计模式之原型模式(prototype)

Java 设计模式之原型学习与掌握