行百里er 行百里er
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)

行百里er

Java程序员一枚
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)
  • 【设计模式】各个击破单例模式的8种写法
  • 【设计模式】策略模式之“这不就是if-else吗”
  • 【设计模式】工厂系列-FactoryMethod,AbstractFactory,Spring IOC
  • 【设计模式】只需体验三分钟,你就会跟我一样了解Facade和Mediator模式
  • 【设计模式】代理模式那些事儿:静态代理,动态代理,JDK的动态代理,cglib,Spring AOP
  • 【设计模式】通俗易懂版责任链模式
  • 【设计模式】模板模式,学会它咱也写出优雅健壮的代码!
  • 【设计模式】好玩的原型模式:Prototype
    • 原型模式
    • 浅克隆
    • 深克隆
    • Prototype模式应用场景
    • 小结
  • 【设计模式】1分钟整明白什么是Builder建造者模式
  • 设计模式
行百里er
2020-09-22
目录

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

作者:行百里er

博客:https://chendapeng.cn (opens new window)

提示

这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!

# 原型模式

原型(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 + '\'' +
                '}';
    }
}
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
64
65
66
67
68
69
70
71
72

运行结果:

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'}}
1
2
3
4
5

从结果可以看到,对象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);
1
2
3
4
5
6
7
8
9
10
11
12
13

结果:

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'}}
1
2
3
4
5

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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Contact implements Cloneable {
    //...

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    //...
}
1
2
3
4
5
6
7
8
9
10

执行如下代码:

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);
1
2
3
4
5
6
7
8
9

结果:

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'}}
1
2
3

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

# Prototype模式应用场景

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

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

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

# 小结

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

首发公众号 行百里er ,欢迎老铁们关注阅读指正。

#设计模式
上次更新: 2022/10/06, 18:01:43
【设计模式】模板模式,学会它咱也写出优雅健壮的代码!
【设计模式】1分钟整明白什么是Builder建造者模式

← 【设计模式】模板模式,学会它咱也写出优雅健壮的代码! 【设计模式】1分钟整明白什么是Builder建造者模式→

最近更新
01
重要数据不能丢!MySQL数据库定期备份保驾护航!
05-22
02
分布式事务解决方案之 Seata(二):Seata AT 模式
09-09
03
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
09-05
更多文章>
Theme by Vdoing | Copyright © 2020-2023 行百里er | MIT License | 豫ICP备2022020385号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式