写点什么

一文读懂深克隆与浅克隆的关系

用户头像
Tom弹架构
关注
发布于: 刚刚

本文节选自《设计模式就该这样学》

1 分析 JDK 浅克隆 API 带来的问题

在 Java 提供的 API 中,不需要手动创建抽象原型接口,因为 Java 已经内置了 Cloneable 抽象原型接口,自定义的类型只需实现该接口并重写 Object.clone()方法即可完成本类的复制。通过查看 JDK 的源码可以发现,其实 Cloneable 是一个空接口。Java 之所以提供 Cloneable 接口,只是为了在运行时通知 Java 虚拟机可以安全地在该类上使用 clone()方法。而如果该类没有实现 Cloneable 接口,则调用 clone()方法会抛出 CloneNotSupportedException 异常。一般情况下,如果使用 clone()方法,则需满足以下条件。


(1)对任何对象 o,都有 o.clone() != o。换言之,克隆对象与原型对象不是同一个对象。


(2)对任何对象 o,都有 o.clone().getClass() == o.getClass()。换言之,复制对象与原对象的类型一样。


(3)如果对象 o 的 equals()方法定义恰当,则 o.clone().equals(o)应当成立。


我们在设计自定义类的 clone()方法时,应当遵守这 3 个条件。一般来说,这 3 个条件中的前 2 个是必需的,第 3 个是可选的。下面使用 Java 提供的 API 应用来实现原型模式,代码如下。



class Client { public static void main(String[] args) { //创建原型对象 ConcretePrototype type = new ConcretePrototype("original"); System.out.println(type); //复制原型对象 ConcretePrototype cloneType = type.clone(); cloneType.desc = "clone"; System.out.println(cloneType);
} static class ConcretePrototype implements Cloneable { private String desc;
public ConcretePrototype(String desc) { this.desc = desc; }
@Override protected ConcretePrototype clone() { ConcretePrototype cloneType = null; try { cloneType = (ConcretePrototype) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return cloneType; }
@Override public String toString() { return "ConcretePrototype{" + "desc='" + desc + '\'' + '}'; } }}
复制代码


super.clone()方法直接从堆内存中以二进制流的方式进行复制,重新分配一个内存块,因此其效率很高。由于 super.clone()方法基于内存复制,因此不会调用对象的构造函数,也就是不需要经历初始化过程。在日常开发中,使用 super.clone()方法并不能满足所有需求。如果类中存在引用对象属性,则原型对象与克隆对象的该属性会指向同一对象的引用。



@Datapublic class ConcretePrototype implements Cloneable {
private int age; private String name; private List<String> hobbies;
@Override public ConcretePrototype clone() { try { return (ConcretePrototype)super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } }
@Override public String toString() { return "ConcretePrototype{" + "age=" + age + ", name='" + name + '\'' + ", hobbies=" + hobbies + '}'; }}
复制代码


修改客户端测试代码。



public static void main(String[] args) { //创建原型对象 ConcretePrototype prototype = new ConcretePrototype(); prototype.setAge(18); prototype.setName("Tom"); List<String> hobbies = new ArrayList<String>(); hobbies.add("书法"); hobbies.add("美术"); prototype.setHobbies(hobbies); System.out.println(prototype); //复制原型对象 ConcretePrototype cloneType = prototype.clone(); cloneType.getHobbies().add("技术控");
System.out.println("原型对象:" + prototype); System.out.println("克隆对象:" + cloneType);
}
复制代码


我们给复制的对象新增一个属性 hobbies(爱好)之后,发现原型对象也发生了变化,这显然不符合预期。因为我们希望复制出来的对象应该和原型对象是两个独立的对象,不再有联系。从测试结果来看,应该是 hobbies 共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值,protoType 和 cloneType 的 hobbies 值都会改变。这就是我们常说的浅克隆。只是完整复制了值类型数据,没有赋值引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?Java 自带的 clone()方法进行的就是浅克隆。而如果我们想进行深克隆,可以直接在 super.clone()后,手动给复制对象的相关属性分配另一块内存,不过如果当原型对象维护很多引用属性的时候,手动分配会比较烦琐。因此,在 Java 中,如果想完成原型对象的深克隆,则通常使用序列化(Serializable)的方式。

2 使用序列化实现深克隆

在上节的基础上继续改造,增加一个 deepClone()方法。



/** * Created by Tom. */@Datapublic class ConcretePrototype implements Cloneable,Serializable {
private int age; private String name; private List<String> hobbies;
@Override public ConcretePrototype clone() { try { return (ConcretePrototype)super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } }
public ConcretePrototype deepClone(){ try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis);
return (ConcretePrototype)ois.readObject(); }catch (Exception e){ e.printStackTrace(); return null; }

}
@Override public String toString() { return "ConcretePrototype{" + "age=" + age + ", name='" + name + '\'' + ", hobbies=" + hobbies + '}'; }}
复制代码


客户端调用代码如下。



public static void main(String[] args) { //创建原型对象 ConcretePrototype prototype = new ConcretePrototype(); prototype.setAge(18); prototype.setName("Tom"); List<String> hobbies = new ArrayList<String>(); hobbies.add("书法"); hobbies.add("美术"); prototype.setHobbies(hobbies);
//复制原型对象 ConcretePrototype cloneType = prototype.deepCloneHobbies(); cloneType.getHobbies().add("技术控");
System.out.println("原型对象:" + prototype); System.out.println("克隆对象:" + cloneType); System.out.println(prototype == cloneType);

System.out.println("原型对象的爱好:" + prototype.getHobbies()); System.out.println("克隆对象的爱好:" + cloneType.getHobbies()); System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
复制代码


运行程序,得到如下图所示的结果,与期望的结果一致。



从运行结果来看,我们的确完成了深克隆。


【推荐】Tom弹架构:收藏本文,相当于收藏一本“设计模式”的书


本文为“Tom 弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!

如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注微信公众号『 Tom 弹架构 』可获取更多技术干货!

发布于: 刚刚阅读数: 3
用户头像

Tom弹架构

关注

不只做一个技术者,更要做一个思考者 2021.10.22 加入

畅销书作者,代表作品: 《Spring 5核心原理与30个类手写实战》 《Netty 4核心原理与手写RPC框架实战》 《设计模式就该这样学》

评论

发布
暂无评论
一文读懂深克隆与浅克隆的关系