【设计模式】好玩的原型模式:Prototype

1,274 阅读5分钟

原型模式

原型(Prototype)模式的定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象

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

比如,用过VMware安装过虚拟机的可能知道,我们可以先安装一个模板机,然后通过克隆模板机创建出很多虚拟机出来,这种采用复制的方法大大提升了效率。

再比如,群发消息的场景,我们希望群发出去的东西title随着发送对象的不同而改变,这时可以构造出一个消息对象,群发复制这个对象,然后title进行个性化定制。

用消息作为原型对象,具体对象来自于拷贝原对象,要完成对象的拷贝,原型类必须实现Cloneable接口,类图如下所示:

浅克隆

原型模式的克隆有 浅克隆深克隆 ,我们通过例子来演示一下。

假设Message类有String类型的title属性、int类型的state属性以及Contact引用类型的contact属性,类图:

public class Message implements Cloneable {
    private String title;
    private int state;
    private Contact contact;

    public Message(String title, int state, Contact contact) {
        this.title = title;
        this.state = state;
        this.contact = contact;
    }

    @Override
    public String toString() {
        return "Message{" +
                "title='" + title + '\'' +
                ", state=" + state +
                ", contact=" + contact +
                '}';
    }

    public static void main(String[] args) {
        try {
            Contact contact = new Contact("abc@1.com", "666");
            Message msg1 = new Message("张三", 1, contact);
            Message msg2 = (Message) msg1.clone();
            System.out.println("msg1:" + msg1);
            System.out.println("msg2:" + msg2);

            System.out.println("contact1 == contact2 ? " + (msg1.contact == msg2.contact));

            msg1.contact.setPhone("888");
            System.out.println("msg1:" + msg1);
            System.out.println("msg2:" + msg2);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

class Contact {
    private String email;
    private String phone;

    public Contact(String email, String phone) {
        this.email = email;
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "Contact{" +
                "email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

运行结果:

msg1:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
contact1 == contact2 ? true
msg1:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}

从结果可以看到,对象msg1的contact和msg2的contact竟然相等,也就是执行了同一个内存地址,而且,修改了msg1的contact属性,msg2的也跟着变了!

这种就属于浅克隆,创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址

再来变一下,msg2克隆出来后,把它的title重新设置一下看看:

Contact contact = new Contact("abc@1.com", "666");
Message msg1 = new Message("张三", 1, contact);
Message msg2 = (Message) msg1.clone();
//重新设置msg1的title
msg1.title = "李四";
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);

System.out.println("contact1 == contact2 ? " + (msg1.contact == msg2.contact));

msg1.contact.setPhone("888");
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);

结果:

msg1:Message{title='李四', state=1, contact=Contact{email='abc@1.com', phone='666'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}
contact1 == contact2 ? true
msg1:Message{title='李四', state=1, contact=Contact{email='abc@1.com', phone='888'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}

msg1和msg2的title就不一样了!

疑问来了,String也不是基本类型啊,msg1的title变了,为什么它就和我自定义的引用类型Contact不一样完全拷贝到msg2呢?

对于String类型,Java就希望你把它 认为是基本类型,它是没有clone方法的,处理机制也比较特殊,通过字符串常量池(stringpool)在需要的时候才在内存中创建新的字符串。

当定义完msg1,并根据msg1拷贝出msg2后,两个title都执行String常量池中的张三,而msg1.title="李四"执行后,msg1就指向了常量池中的李四;对于Contact类型,这是一个自定义的引用,在内存中就是那个地址,因此,msg1.contact==msg2.contact,即执行同一个内存地址!

深克隆

很多时候,我们当然不希望浅克隆,比如上面的案例中,每个msg对象的contact都不应该是一样的,这就需要深克隆的存在了。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

那么就让Contact类也实现Cloneable接口,同时Message类的clone方法要对引用也克隆一份:

public class Message implements Cloneable {
    private String title;
    private int state;
    private Contact contact;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Message msg = (Message) super.clone();
        //将引用对象也克隆一份
        msg.contact = (Contact) contact.clone();
        return msg;
    }
}
class Contact implements Cloneable {
    //...

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    //...
}

执行如下代码:

Contact contact = new Contact("abc@1.com", "666");
Message msg1 = new Message("张三", 1, contact);
Message msg2 = (Message) msg1.clone();

System.out.println("contact1 == contact2 ? " + (msg1.contact == msg2.contact));

msg1.contact.setPhone("888");
System.out.println("msg1:" + msg1);
System.out.println("msg2:" + msg2);

结果:

contact1 == contact2 ? false
msg1:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='888'}}
msg2:Message{title='张三', state=1, contact=Contact{email='abc@1.com', phone='666'}}

contact1 == contact2为false了,因为它们指向了不同的内存地址了,此时修改msg1的contact属性,msg2随之而改变。

Prototype模式应用场景

原型模式通常适用于以下场景。

  • 对象之间相同或相似,即只是个别的几个属性不同的时候。
  • 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
  • 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
  • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。

在Spring中,如果一个类被标记为prototype,每一次请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)都会产生一个新的bean实例。

小结

  • Java自带原型模式,Object类提供了clone方法
  • 要实现原型模式必须实现Cloneable接口
  • 重写clone方法,如果之重写clone,而未实现Cloneable接口,调用时会出现异常
  • 该模式用于对一个对象的属性已确定,需产生很多相同对象的时候
  • 注意区分深克隆与浅克隆

点个赞再走吧~

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿