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. BUUCTF-web ikun(Python 反序列化)

    正如本题所说,脑洞有点大.考点还很多,不过最核心的还是python的pickle反序列化漏洞 题目中暗示了要6级号,找了很多页都没看到,于是写了脚本 在第180页有6级号,但是价格出奇的高,明显买不起 ...

  2. 【扩展推荐】Intervention/image 图片处理

    Intervention/image 是为 Laravel 定制的图片处理工具, 它提供了一套易于表达的方式来创建.编辑图片. 一.环境要求 二.安装及配置 下载地址:https://packagis ...

  3. WEB简单的登录注册功能(分层)

    登录: 前端页面: <body> <form action="/webtext/LogingServlet" method="post"> ...

  4. Mac Sourcetree克隆项目提示无效的url

    之前用SoucreTree拉去过另一个账号的git项目,今天创建了一个新的码云账号,克隆里面的项目是一直报错误 > 错误如下: > 原因以及解决方案:

  5. feign.FeignException: status 404 reading xxService#xxmethod

    做乐优商城授权中心出错 public interface UserApi { @GetMapping("query") public User queryUser( @Reques ...

  6. 1_Java语言概述

    学于尚硅谷开源课程 宋红康老师主讲 感恩 尚硅谷官网:http://www.atguigu.com 尚硅谷b站:https://space.bilibili.com/302417610?from=se ...

  7. 老男孩Django笔记(非原创)

    .WEB框架 MVC Model View Controller 数据库 模板文件 业务处理 MTV Model Template View 数据库 模板文件 业务处理 ############## ...

  8. PHP jddayofweek() 函数

    ------------恢复内容开始------------ 实例 返回 1998 年 1 月 13 日这天是周几: <?php$jd=gregoriantojd(1,13,1998);echo ...

  9. PHP str_split() 函数

    实例 把字符串 "Hello" 分割到数组中: <?php print_r(str_split("Hello")); ?>高佣联盟 www.cgew ...

  10. Python 图像处理 OpenCV (16):图像直方图

    前文传送门: 「Python 图像处理 OpenCV (1):入门」 「Python 图像处理 OpenCV (2):像素处理与 Numpy 操作以及 Matplotlib 显示图像」 「Python ...