网站首页 > java教程 正文
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为模板进行克隆。
这种方法的弊端:
- 创建新对象, 总是需要重新获取原始对象的属性值 ,效率低;
- 总是需要重新初始化对象,而不是动态获取对象运行时的状态,不灵活。(什么意思呢,比如原始的 Sheep 有一项要修改,那么剩下的以它为范本的,必然要重新初始化)
一、原型模式
- 原型模式指的是,用原型实例指定创建对象的种类,并通过拷贝这些原型,创建新的对象;
- 原型模式是一种创建型设计模式,允许一个对象再创建另一个可以定制的对象,无需知道如何创建的细节;
- 工作原理是:发动创建的这个对象,请求原型对象,让 原型对象来自己实施创建 ,就是 原型对象.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):
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是复制一份给新对象;
- 对于数据类型是引用数据类型的成员变量,浅拷贝会进行引用传递,也就是只是将地址指针复制一份给新对象,实际上复制前和复制后的内容都指向同一个实例。这种情况,显然在一个对象里修改成员变量,会影响到另一个对象的成员变量值(因为修改的都是同一个)
- 默认的 clone() 方法就是浅拷贝。
在源码里也说明了,这个方法是 shallow copy 而不是 deep copy 。
在实际开发中,往往是希望克隆的过程中,如果类的成员是引用类型,也能完全克隆一份,也就是所谓的 深拷贝 。
深拷贝(Deep Copy):
- 复制对象的所有基本数据类型成员变量值;
- 为所有 引用数据类型 的成员变量申请存储空间,并且也 复制每个 引用数据类型的成员变量 引用的 所有对象 ,一直到该对象可达的所有对象;
深拷贝的实现方式,需要通过重写 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;
三、总结
原型模式:
- 当需要创建一个新的对象的内容比较复杂的时候,可以利用原型模式来简化创建的过程,同时能够提高效率。
- 因为这样不用重新初始化对象,而是动态地获得对象运行时的状态,如果原始的对象内部发生变化,其他克隆对象也会发生相应变化,无需一 一修改。
- 实现深拷贝的方法要注意。
缺点:
每一个类都需要一个克隆方法,对于全新的类来说不是问题,但是如果是用已有的类进行改造,那么可能会因为要修改源代码而违背 OCP 原则。
文末福利:
最近整理了:Java零基础精品教学视频,技术书籍,技术文档,面试文档,学习脑图,基础核心知识等..学习资料!!
私信回复关键词:Java (点击我的头像,进入主页面,点击右上角私信按钮)即可免费领取。
希望大家将此篇文章分享,转载,让更多需要的朋友看到,这样不仅帮助了自己,也帮助了他人,谢谢!!
猜你喜欢
- 2024-10-19 Python 中赋值、浅拷贝、深拷贝的区别是什么?
- 2024-10-19 Java 对象拷贝原理剖析(java 对象拷贝原理剖析)
- 2024-10-19 认识Object类和深浅拷贝!(阐述object.assign的用法,深拷贝与浅拷贝的区别?)
- 2024-10-19 三面“有赞”Java岗斩获offer:Spring+JVM+并发锁+分布式+算法
- 2024-10-19 谈谈 Java 开发中的对象拷贝(java对象拷贝工具类)
- 2024-10-19 深入浅出Java中的clone克隆方法,写得也太棒了
- 2024-10-19 深拷贝和浅拷贝之list、dataframe
- 2024-10-19 对象拷贝java 浅谈(java对象的拷贝)
- 2024-10-19 Java克隆对象需要知道的事(java克隆的作用)
- 2024-10-19 Java的Object十二个常用方法,你用过几个?
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)