0、背景


克隆羊问题:有一个羊,是一个类,有对应的属性,要求创建完全一样的10只羊出来。

那么实现起来很简单,我们先写出羊的类:

public class Sheep {
private String name;
private int age;
private String color;
//下面写上对应的get和set方法,以及对应的构造器
}

然后,创建10只一样的羊,就在客户端写一个代码创建:

 //原始羊
Sheep sheep = new Sheep("tom",1,"白色");
//克隆羊
Sheep sheep1 = new Sheep(sheep.getName(),sheep.getAge(),sheep.getColor());

sheep1 是克隆的第一只羊,接着就可以复制十遍这个代码,然后命名不同的羊,以原始sheep为模板进行克隆。

这种方法的弊端:

  1. 创建新对象,总是需要重新获取原始对象的属性值,效率低;
  2. 总是需要重新初始化对象,而不是动态获取对象运行时的状态,不灵活。(什么意思呢,比如原始的 Sheep 有一项要修改,那么剩下的以它为范本的,必然要重新初始化)


一、原型模式


  1. 原型模式指的是,用原型实例指定创建对象的种类,并通过拷贝这些原型,创建新的对象;
  2. 原型模式是一种创建型设计模式,允许一个对象再创建另一个可以定制的对象,无需知道如何创建的细节;
  3. 工作原理是:发动创建的这个对象,请求原型对象,让原型对象来自己实施创建,就是原型对象.clone()

如下类图所示:

其中,Prototype 是一个原型接口,在这里面把克隆自己的方法声明出来;

ConcreteProtype 可以是一系列的原型类,实现具体操作。

java 的 Object 类是所有类的根类,Object提供了一个 clone() 方法,该方法可以将一个对象复制一份,但是想要实现 clone 的 java 类必须要实现 Cloneable 接口,实现了之后这个类就具有复制的能力。

对于克隆羊问题,我们来利用原型设计模式进行改进:

让Sheep类,实现 Cloneable 接口:

public class Sheep implements Cloneable{
private String name;
private int age;
private String color; //getters&&setters&&constructors @Override
protected Object clone() {
Sheep sheep = null;
try {
sheep = (Sheep)super.clone();//使用默认Object的clone方法来完成
} catch (CloneNotSupportedException e) {
System.out.println(e.getMessage());
}
return sheep;
}
}

现在的 Sheep 类就是一个具体的原型实现类了,我们想要克隆的时候,客户端调用可以这样:

Sheep sheep1 = (Sheep) sheep.clone();
Sheep sheep2 = (Sheep) sheep.clone();
//。。。。。类似

这种做法就是原型设计模式。

(spring框架里,通过bean标签配置类的scope为prototype,就是用的原型模式)


二、原型模式的浅拷贝、深拷贝问题


使用上面所说的原型模式,按理说是复制出了一模一样的对象。

但我们做一个尝试,如果 sheep 类里的成员变量有一个是对象,而不是基础类型呢

private Sheep friend;

然后我们创建、再克隆:

Sheep sheep = new Sheep("tom",1,"白色");//原始羊
sheep.setFriend(new Sheep("jack",2,"黑色"));
Sheep sheep1 = (Sheep) sheep.clone();
Sheep sheep2 = (Sheep) sheep.clone();
Sheep sheep3 = (Sheep) sheep.clone();

重写一下 Sheep 类的 toString 方法,输出信息和对应的属性的 hashcode 后会发现:

Sheep{name='tom', age=1, color='白色', friend=488970385}
Sheep{name='tom', age=1, color='白色', friend=488970385}
Sheep{name='tom', age=1, color='白色', friend=488970385}

friend 的 hashCode 值都一样,也就是克隆的类的 friend 属性其实没有被复制,而是指向了同一个对象。

这就叫浅拷贝(shallow copy):

  1. 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是复制一份给新对象;
  2. 对于数据类型是引用数据类型的成员变量,浅拷贝会进行引用传递,也就是只是将地址指针复制一份给新对象,实际上复制前和复制后的内容都指向同一个实例。这种情况,显然在一个对象里修改成员变量,会影响到另一个对象的成员变量值(因为修改的都是同一个)
  3. 默认的 clone() 方法就是浅拷贝。

在源码里也说明了,这个方法是shallow copy 而不是 deep copy

在实际开发中,往往是希望克隆的过程中,如果类的成员是引用类型,也能完全克隆一份,也就是所谓的深拷贝

深拷贝(Deep Copy):

  1. 复制对象的所有基本数据类型成员变量值;
  2. 为所有 引用数据类型 的成员变量申请存储空间,并且也复制每个 引用数据类型的成员变量 引用的 所有对象,一直到该对象可达的所有对象;

深拷贝的实现方式,需要通过重写 clone 方法,或者通过对象的序列化。

下面来实现一下。


2.1 通过重写 clone 方法深拷贝

/*
被拷贝的类引用的类,此类的clone用默认的clone即可
*/
public class CloneTarget implements Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass; public CloneTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/*
原型类,其中有成员是引用类型,因此clone方法要重写达到深拷贝
*/
public class Prototype implements Cloneable {
public String name;
public CloneTarget cloneTarget;
public Prototype() {
super();
} @Override
protected Object clone() throws CloneNotSupportedException {
Object o = null;
//用了浅拷贝,基本数据克隆完成,但是cloneTarget指向的还是原来的对象
o = super.clone();
//单独处理引用类型
Prototype target = (Prototype) o;
target.cloneTarget = (CloneTarget)cloneTarget.clone();
return target;
}
}

这样的话,新建一个原型Prototype的对象后,对他进行克隆,得到的里面的 CloneTarget 成员也是深拷贝的两个不一样的对象了。

但是这种方法本质上是相当于 套娃 ,因为都要单独处理重写 clone 方法,所以有些麻烦。


2.2 通过对象的序列化

在 Prototype 里直接 使用序列化+反序列化,达到对这个对象整体的一个复制。

另外注意,序列化和反序列化,必须实现 Serializable 接口,所以 implements 后面不止要有 Cloneable,还有Serializable。

//利用序列化实现深拷贝
public Object deepClone(){
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
Prototype copy = (Prototype) ois.readObject();
return copy;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}

然后我们想要克隆的时候,直接调用这个 deepClone 方法就可以达到目的。

忽视掉里面的 try - catch 之类的代码,其实核心部分就是用到序列化和反序列化的总共 4 个对象。这种方法是推荐的,因为实现起来更加容易。

序列化反序列化达到深拷贝目的的原理:

  • ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream,但是只能将支持 java.io.Serializable 接口的对象写入流中。

在这里,我们采用的OutputStream是ByteArrayOutputStream——字节数组输出流,通过创建的ObjectOutputStream的writeObject方法,把对象写进了这个字节数组输出流。

  • 相对应的,ObjectInputStream反序列化原始数据,恢复以前序列化的那些对象。

在这里,把字节数组重新构造成一个ByteArrayInputStream——字节数组输入流,通过ObjectInputStream的readObject方法,把输入流重新构造成一个对象。

结合上面的代码再看看:

bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);//写入指定的OutputStream
oos.writeObject(this);//把对象写入到输出流中,整个对象,this bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);//读取指定的InputStream
Prototype copy = (Prototype) ois.readObject();//从输入流中读取一个对象 return copy;


三、总结


原型模式:

  1. 当需要创建一个新的对象的内容比较复杂的时候,可以利用原型模式来简化创建的过程,同时能够提高效率。
  2. 因为这样不用重新初始化对象,而是动态地获得对象运行时的状态,如果原始的对象内部发生变化,其他克隆对象也会发生相应变化,无需一 一修改。
  3. 实现深拷贝的方法要注意。

缺点:

每一个类都需要一个克隆方法,对于全新的类来说不是问题,但是如果是用已有的类进行改造,那么可能会因为要修改源代码而违背 OCP 原则。

设计模式:原型模式介绍 && 原型模式的深拷贝问题的更多相关文章

  1. 企业IT管理员IE11升级指南【4】—— IE企业模式介绍

    企业IT管理员IE11升级指南 系列: [1]—— Internet Explorer 11增强保护模式 (EPM) 介绍 [2]—— Internet Explorer 11 对Adobe Flas ...

  2. weblogic开发模式与生产模式介绍

    weblogic开发模式与生产模式介绍 开发模式:该模式启用自动部署 生产模式:该模式关闭自动部署 weblogic server 三种部署方法:自动部署.控制台部署.命令部署 自动部署:当其处于启用 ...

  3. Docker网络模式介绍

    一.概述 docker的网络驱动有很多种方式,按照docker官网给出的网络解决方案就有6种,分别是:bridge.host.overlay.macvlan.none.Network plugins, ...

  4. CLR的GC工作模式介绍(Workstation和Server)

    CLR的核心功能之一就是垃圾回收(garbage collection),关于GC的基本概念本文不在赘述.这里主要针对GC的两种工作模式展开讨论和研究. Workstaction模式介绍 该模式设计的 ...

  5. C#设计模式:原型模式(Prototype)及深拷贝、浅拷贝

    原型模式(Prototype) 定义: 原型模式:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象.被复制的实例被称为原型,这个原型是可定制的. Prototype Pattern也是一 ...

  6. 设计模式(六)原型模式(Prototype Pattern)

    一.引言 在软件系统中,当创建一个类的实例的过程很昂贵或很复杂,并且我们需要创建多个这样类的实例时,如果我们用new操作符去创建这样的类实例,这未免会增加创建类的复杂度和耗费更多的内存空间,因为这样在 ...

  7. 设计模式(五)——原型模式(加Spring框架源码分析)

    原型模式 1 克隆羊问题 现在有一只羊 tom,姓名为: tom, 年龄为:1,颜色为:白色,请编写程序创建和 tom 羊 属性完全相同的 10 只羊. 2 传统方式解决克隆羊问题 1) 思路分析(图 ...

  8. 小菜学习设计模式(四)—原型(Prototype)模式

    前言 设计模式目录: 小菜学习设计模式(一)—模板方法(Template)模式 小菜学习设计模式(二)—单例(Singleton)模式 小菜学习设计模式(三)—工厂方法(Factory Method) ...

  9. 设计模式学习系列6 原型模式(prototype)

    原型模式(prototype)用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.允许一个对象再创建另外一个新对象的时候根本无需知道任何创建细节,只需要请求圆形对象的copy函数皆可. 1 ...

随机推荐

  1. 机器学习 | SVD矩阵分解算法,对矩阵做拆分,然后呢?

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是机器学习专题第28篇文章,我们来聊聊SVD算法. SVD的英文全称是Singular Value Decomposition,翻译过来 ...

  2. CF940E Cashback 线段树优化DP

    题目描述 Since you are the best Wraith King, Nizhniy Magazin «Mir» at the centre of Vinnytsia is offerin ...

  3. 循序渐进nginx(一):介绍、安装、hello world、Location匹配

    目录 前言: Nginx是什么 使用场景: 官方文档说明 安装 windows下: linux(CentOS7)下: docker下: 目录结构 Hello World 1.展示一下默认的核心配置: ...

  4. Linux系统中(CentOS 7)的用户和权限管理

    目录 用户和组 用户信息文件 用户密码信息 相关命令 用户管理 组管理 密码管理 权限管理 文件的详细信息 文件权限 相关命令 用户和组 用户信息文件 /etc/passwd (1 2 3 4 5 6 ...

  5. ajax提交表单,包括跳入的坑!

    本来今天上午写了一个js执行上下文的一个了解.但是写一大半的时候出去有事,电脑关了啥都没了. 还是让我们进入正题 ajax提交表单,很简单,原生js的代码太复杂,我们就jq的去写. 创建html文件, ...

  6. APP自动化 -- 滑动解锁、滑动验证

    一.解锁 1.代码 2.效果 1)执行效果 2)点位效果

  7. Go语言的跳跃表(SkipList)实现

    之所以会有这篇文章,是因为我在学习Go语言跳表代码实现的过程中,产生过一些困惑,但网上的大家都不喜欢写注释- - 我的代码注释一向是写的很全的,所以发出来供后来者学习参考. 本文假设你已经理解了跳表的 ...

  8. ~~并发编程(十四):Queue~~

    进击のpython ***** 并发编程--Queue 进程其实就提过这个Queue的问题,我们为什么在进程使用Queue? 是因为当时我们想要对共享数据进行修改,同时也希望它能够自动的给我加个锁 基 ...

  9. Windows下给PHP安装redis扩展

    一.选择适合的版本 二.下载扩展 官网下载地址:http://pecl.php.net/package/redis ,选择合适的版本进行下载 三.解压后将下面两个文件复制到PHP扩展文件目录(ext文 ...

  10. Python 数字数据类型

    数字数据类型,包括整数.浮点数.复数和布尔类型. 整数 int 长整型(数字长度不限制):包括正数,负数,0. # 正数 num_int = 123 print(type(num_int)) # &l ...