参考资料:
目录
1、为什么既要继承Cloneable接口也要重写clone方法
一、深克隆与浅克隆介绍
1、概述
java中允许将一个对象赋值出一份完全一样的对象出来,称之为克隆。克隆又分浅克隆与深克隆,他们的区别在于,对于引用类型的成员变量,浅克隆复制的是引用,而深克隆复制的是对象。
浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的。
简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象。
深克隆(Deep Clone)是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象。
2、拷贝功能的实现
要实现克隆则需要继承Cloneable接口,并重写Object 类中 clone()方法。
public class ProblemSolution {
public static void main(String[] args) {
// 创建被赋值对象
People p1 = new People();
p1.setId(1);
p1.setName("Java");
// 克隆 p1 对象
People p2;
try {
p2 = (People) p1.clone();
System.out.println(p1.getName().equals(p2.getName()));
System.out.println(p1.getName()==p2.getName());
}
catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class People implements Cloneable {
// 属性
private Integer id;
private String name;
/**
* 重写 clone 方法
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
结果如下:
true
true
上面的代码使用了Object类自带的clone方法实现了clone,我们可以从结果中看到,两个对象中的String类型引用指向的是同一个对象,因此Object的clone方法是浅拷贝。
二、深克隆实现方式
1、将所有对象都实现克隆
这种方式我们需要修改 People 和 Address 类,让它们都继承Cloneable接口并重写clone方法,让所有的引用对象都实现克隆,从而实现 People 类的深克隆。
class People implements Cloneable {
private Address address;
// 重写clone方法,内部将每一个成员变量都进行clone
@Override
protected Object clone() throws CloneNotSupportedException {
People people = (People) super.clone();
people.setAddress((Address)this.address.clone()); // 引用类型克隆赋值
return people;
}
public void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}
class Address implements Cloneable {
private String city;
// 重写clone方法
@Override
protected Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
public Address(String city){
this.city = city;
}
// 重写equals方法
@Override
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof Address) {
Address anotherAddress = (Address)anObject;
return this.getCity().equals(anotherAddress.getCity());
}
return false;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
将p1与其clone得到的p2进行比较。
public static void main(String[] args) {
// 创建被赋值对象
People p1 = new People();
p1.setAddress(new Address("北京"));
// 克隆 p1 对象
People p2;
try {
p2 = (People) p1.clone();
System.out.println(p1.getAddress().equals(p2.getAddress()));
System.out.println(p1.getAddress()==p2.getAddress());
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
结果如下:
true
false
我们看到p1与p2中的成员变量address已经是不同的对象了,深克隆实现成功。但这么做的问题在于所有的类都要实现clone方法,过于繁琐。
2、通过构造方法实现深克隆
《Effective Java》 中推荐使用构造器(Copy Constructor)来实现深克隆,如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新 new 一个对象。
public static void main(String[] args) {
// 创建被赋值对象
People p1 = new People(new Address("北京"));
People p2 = new People(new Address(p1.getAddress().getCity()));
System.out.println(p1.getAddress().equals(p2.getAddress()));
System.out.println(p1.getAddress()==p2.getAddress());
}
结果如下:
true
false
3、通过字节流实现深克隆
通过 JDK 自带的字节流实现深克隆的方式,是先将要原型对象写入到内存中的字节流,然后再从这个字节流中读出刚刚存储的信息,来作为一个新的对象返回,那么这个新对象和原型对象就不存在任何地址上的共享,这样就实现了深克隆。
简单来说,就是使用序列化存储后再读取,就可以获得一个完全一致的对象。
class Address implements Serializable{
...
}
class People implements Serializable{
...
}
class StreamClone {
public static <T extends Serializable> T clone(People obj) {
T cloneObj = null;
try {
// 写入字节流
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bo);
oos.writeObject(obj);
oos.close();
// 分配内存,写入原始对象,生成新对象
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流
ObjectInputStream oi = new ObjectInputStream(bi);
// 返回生成的新对象
cloneObj = (T) oi.readObject();
oi.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
public static void main(String[] args) {
// 创建被赋值对象
People p1 = new People(new Address("北京"));
People p2 = StreamClone.clone(p1);
System.out.println(p1.getAddress().equals(p2.getAddress()));
System.out.println(p1.getAddress()==p2.getAddress());
}
结果如下:
true
false
需要注意的是,由于是通过字节流序列化实现的深克隆,因此每个对象必须能被序列化,必须实现 Serializable 接口,标识自己可以被序列化,否则会抛出NotSerializableException异常。
4、使用开源工具包
这里以fastjson为例,使用 JSON 工具类会先把对象转化成字符串,再从字符串转化成新的对象,因为新对象是从字符串转化而来的,因此不会和原型对象有任何的关联,这样就实现了深克隆,其他类似的 JSON 工具类实现方式也是一样的。
public static void main(String[] args) {
// 创建被赋值对象
People p1 = new People(new Address("北京"));
Gson gson = new Gson();
People p2 = gson.fromJson(gson.toJson(p1), People.class);
System.out.println(p1.getAddress().equals(p2.getAddress()));
System.out.println(p1.getAddress()==p2.getAddress());
}
补充:
1、为什么既要继承Cloneable接口也要重写clone方法
对于克隆为什么要这样设计,官方没有直接给出答案,我们只能凭借一些经验和源码文档来试着回答一下这个问题。
从源码中可以看出 Cloneable 接口在JDK 1.0 就已经存在了,因此从那个时候就已经有克隆方法了,那我们怎么来标识一个类级别对象拥有克隆方法呢?克隆虽然重要,但我们不能给每个类都默认加上克隆,这显然是不合适的,那我们能使用的手段就只有这几个了:
- 在类上新增标识,此标识用于声明某个类拥有克隆的功能,像 final 关键字一样;
- 使用 Java 中的注解;
- 实现某个接口;
- 继承某个类。
先说第一个,为了一个重要但不常用的克隆功能, 单独新增一个类标识,这显然不合适;再说第二个,因为克隆功能出现的比较早,那时候还没有注解功能,因此也不能使用;第三点基本满足我们的需求,第四点和第一点比较类似,为了一个克隆功能需要牺牲一个基类,并且 Java 只能单继承,因此这个方案也不合适。采用排除法,无疑使用实现接口的方式是那时最合理的方案了,而且在 Java 语言中一个类可以实现多个接口。
那为什么要在 Object 中添加一个 clone() 方法呢?
因为 clone() 方法对时间和效率的要求很高,因此最好能有 JVM 的直接支持,既然要 JVM 直接支持,就要找一个 API 来把这个方法暴露出来才行,最直接的做法就是把它放入到一个所有类的基类 Object 中,这样所有类就可以很方便地调用到了。
2、重写clone() 方法有哪些要求
首先看下源码,clone()是使用 native 修饰的本地方法,因此执行的性能会很高,并且它返回的类型为 Object,因此在调用克隆之后要把对象强转为目标类型才行。虽然我们看不到clone()方法的具体实现,但我们可以从注释中看出一些信息。
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
* <p>
* ......
*/
protected native Object clone() throws CloneNotSupportedException;
从注释中可以看出:
(1)对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象;
(2)对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回true,因为克隆对象与原对象的类型是一样的;
(3)对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。