转载

JAVA设计模式之:原型模式

一、定义

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

二、应用场景

我们先看下下面这个示例:

public class User {

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;
    
    ...
}
public class Client {

    public static void main(String[] args) {

        User user1 = new User();
        user1.setAge(22);
        user1.setSex("男");
        user1.setNickname("Theshy");

        User user2 = new User();
        user2.setAge(22);
        user2.setSex("男");
        user2.setNickname("Theshy");

        User user3 = new User();
        user3.setAge(22);
        user3.setSex("男");
        user3.setNickname("Theshy");
    }
}

在示例中我们看到有三个用户,并且三个用户的信息都是一致的,粗略一看好像没什么问题,但是假如我们有100个同样信息的用户,难道我们就new100个对象出来吗?这显然是不可能的。恰巧原型模式就能帮助我们解决这样的问题。

原型模式主要适用于以下场景:

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

三、原型模式的通用写法

一个标准的原型模式代码,应该是这样设计的。先创建原型Prototype接口:

public interface Prototype<T> {
    T clone();
}
public class User implements Prototype<User>{

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;

    ...

    @Override
    public User clone() {
        User user = new User();
        user.setAge(this.age);
        user.setNickname(this.nickname);
        user.setSex(this.sex);
        user.setHobbyList(this.hobbyList);
        return user;
    }

    @Override
    public String toString() {
        return "User{" + "age=" + age + ", nickname='" + nickname + '/'' + ", sex='" + sex + '/'' + ", hobbyList=" + hobbyList + '}';
    }
}
public class Client {

    public static void main(String[] args) {
        //创建原型对象
        User user1 = new User();
        user1.setAge(18);
        user1.setNickname("Theshy");
        user1.setSex("男");
        //clone出来的对象
        User user2 = user1.clone();
      
        System.out.println(user1);
        System.out.println(user2);
    }
}

运行结果

User{age=18, nickname='Theshy', sex='男', hobbyList=null}
User{age=18, nickname='Theshy', sex='男', hobbyList=null}

这时候,可能有有会问了,原型模式就这么简单吗?对,就这么简单。在这个简单的场景之下,看上去操作好像变复杂了。但如果有几百个属性需要复制,那我们就可以一劳永逸。

JAVA设计模式之:原型模式

(该UML图与示例无关)

从 UML 图中,我们可以看到,原型模式 主要包含三个角色:

客户(Client):客户类提出创建对象的请求。
抽象原型(Prototype):规定克隆接口。
具体原型(ClonePrototype):被克隆的对象

虽然上面的复制过程是我们自己完成的,但是在实际编码中,我们一般不会浪费这样的体力劳动,JDK已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。我们改造一下代码。

package com.ksq.shallow;

import java.util.List;

/**
 * Create By Ke Shuiqiang 2020/3/11 21:34
 */
public class User implements Cloneable{

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;

    ...

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

    @Override
    public String toString() {
        return "User{" + "age=" + age + ", nickname='" + nickname + '/'' + ", sex='" + sex + '/'' + ", hobbyList=" + hobbyList + '}';
    }
}

重新运行得到了同样的结果

User{age=18, nickname='Theshy', sex='男', hobbyList=null}
User{age=18, nickname='Theshy', sex='男', hobbyList=null}

从运行结果看,明明原型对象和克隆对象不是同一个对象,下面我们再做个测试,修改克隆对象的年龄,昵称,并为hobbyList添加元素

public class ShallowCloneTest {

    public static void main(String[] args) {

        //创建原型对象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);

        //创建克隆对象
        User clonetype = prototype.clone();
        
        //修改克隆对象的年龄,昵称,并添加一个爱好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看书");
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + clonetype);
        System.out.println("原型对象 == 克隆对象?" + (prototype == clonetype));

    }
}

运行结果

原型对象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳, 看书]}
克隆对象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看书]}
原型对象 == 克隆对象?false

从运行结果看,原型对象和克隆对象的确不是同一个对象,并且克隆对象的年龄和昵称也修改成功了,可是在爱好上好像出了点问题,我明明只为克隆对象新增了爱好,结果原型对象的爱好也新增,这是怎么回事呢?

这里涉及到了原型模式的两个概念,浅克隆和深克隆:

浅克隆:只负责克隆按值传递的数据(比如基本数据类型、String类型),而不克隆它引用的对象,换言之,克隆对象的引用对象都仍然指向原来的引用对象的内存地址。
深克隆:除了克隆按值传递的数据,同时也克隆引用类型对象数据,而不是对象的引用指向原来引用对象的内存地址。

实现深克隆的方式有两种,一种是通过序列化(二进制流),还有一种是通过JSONObject实现(反射)。

序列化实现深克隆

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

    public static void main(String[] args) {

        //创建原型对象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);

        //创建克隆对象
        User clonetype = prototype.deepClone();

        //修改克隆对象的年龄,昵称,并添加一个爱好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看书");
        
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + clonetype);
        System.out.println("原型对象 == 克隆对象?" + (prototype == clonetype));

    }
}

运行结果

原型对象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳]}
克隆对象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看书]}
原型对象 == 克隆对象?false

从运行结果上可以看出两个hobbyList变量是相互独立的,在修改克隆对象的hobbyList时,原型对象并没有改变。这样就达到了深克隆的目的。

让我们看看深克隆对引用对象有哪些要求。假设现在User对象中添加一个Size属性;

public class Size{

    private String height;
    private String weight;

    public String getHeight() { return height; }
    public void setHeight(String height) { this.height = height; }
    public String getWeight() { return weight; }
    public void setWeight(String weight) { this.weight = weight; }

    @Override
    public String toString() {
        return "Size{" +
                "height='" + height + '/'' +
                ", weight='" + weight + '/'' +
                '}';
    }
}
public class User implements Cloneable, Serializable{

    private int age;
    private String nickname;
    private String sex;
    private List<String> hobbyList;
    private Size size;
    
    ...
}
public class DeepCloneTest {

    public static void main(String[] args) {

        //创建原型对象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);
        //添加一个Size对象用于表示用户的身高和体重
        Size size = new Size();
        size.setHeight("180cm");
        size.setWeight("70Kg");
        prototype.setSize(size);

        //创建克隆对象
        User clonetype = prototype.deepClone();

        //修改克隆对象的年龄,昵称,并添加一个爱好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看书");
        //修改克隆对象的体重
        clonetype.getSize().setWeight("80Kg");
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + clonetype);
        System.out.println("原型对象 == 克隆对象?" + (prototype == clonetype));

    }
}

运行结果

JAVA设计模式之:原型模式

结果显示报错,Size对象没有实现Serializable接口,这说明在想要实现深克隆,从原型对象中的引用对象到该引用对象中的引用对象,都必须实现Serializable,或者引用对象使用transient关键字,(这个层次可能会很深,直到你底层对象都实现了Serializable接口,或者是基本数据类型)

使用transient关键字

private transient Size size;

运行结果

原型对象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳], size=Size{height='180cm', weight='70Kg'}}
克隆对象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看书], size=null}
原型对象 == 克隆对象?false

结果显示克隆对象的Size对象的为NULL,很显然原型对象的Size对象没有参与到序列化。

实现Serializable接口

public class Size implements Serializable

运行结果

原型对象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳], size=Size{height='180cm', weight='70Kg'}}
克隆对象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看书], size=Size{height='180cm', weight='80Kg'}}
原型对象 == 克隆对象?false

可以从运行结果上看出Size对象也被克隆,并且修改克隆对象weight属性也成功了。

JSONObject实现深克隆

public class Test {

    public static void main(String[] args) {

        //创建原型对象
        User prototype = new User();
        prototype.setAge(18);
        prototype.setNickname("Theshy");
        prototype.setSex("男");

        List<String> hobbyList = new ArrayList<>();
        hobbyList.add("打球");
        hobbyList.add("游泳");
        prototype.setHobbyList(hobbyList);

        Size size = new Size();
        size.setHeight("180cm");
        size.setWeight("70Kg");
        prototype.setSize(size);

        //import com.alibaba.fastjson.JSONObject;
        //通过JSONObject实现克隆
        User clonetype = JSONObject.parseObject(JSONObject.toJSONString(prototype), User.class);
        //修改克隆对象的年龄,昵称,并添加一个爱好
        clonetype.setAge(22);
        clonetype.setNickname("zhangsan");
        clonetype.getHobbyList().add("看书");
        clonetype.getSize().setWeight("80Kg");
        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + clonetype);
        System.out.println("原型对象 == 克隆对象?" + (prototype == clonetype));

    }
}

运行结果

原型对象:User{age=18, nickname='Theshy', sex='男', hobbyList=[打球, 游泳], size=Size{height='180cm', weight='70Kg'}}
克隆对象:User{age=22, nickname='zhangsan', sex='男', hobbyList=[打球, 游泳, 看书], size=Size{height='180cm', weight='80Kg'}}
原型对象 == 克隆对象?false

运行结果与通过序列化方式的结果是一样的,并且通过JSONObject实现的深克隆,原型对象中的引用对象不需要Serializable接口,但是因为JSONObject是通过反射实现的,所以在性能上相对没有序列化方式高。

四、原型模式破坏单例

既然原型模式是通过二进制流的形式实现克隆的,那么他是否能破坏单例呢?答案是可以的。

@Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
public static void main(String[] args) throws Exception {
        LazyStaticInnerClassSingleton instance = LazyStaticInnerClassSingleton.getInstance();
        LazyStaticInnerClassSingleton clone = (LazyStaticInnerClassSingleton)instance.clone();
        System.out.println("instance == clone ? " + (instance == clone));
    }

运行结果

instance == clone ? false

从结果上看,单例的确被破坏了,其实防止单例被破坏很简单,禁止克隆便可。要么我们的单例类不实现 Cloneable 接口;要么我们重写clone()方法,在clone方法中返回单例对象即可。

@Override 
protected Object clone() throws CloneNotSupportedException{ 
    return INSTANCE;
}

有一点需要注意的是没有人会在一个类是单例的情况下同时又让这个类是原型模式。因为它们两个模式本就是相互矛盾的。

五、原型模式的优缺点

优点

1、性能优良,Java自带的 原型模式 是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。

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

缺点

1、需要为每一个类配置一个克隆方法。

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

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

六、总结

原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。
原文  https://segmentfault.com/a/1190000021997463
正文到此结束
Loading...