个人博客:www.letus179.com
需要克隆的原因
在工作中我们有时会遇到这样的需求:
A对象包含一些有用信息,这时候需要一个和A完全相同的B对象。拿到B对象后,只需要稍微调整下就ok。A和B是两个独立的对象,只是B的初始值来自于A。而A/B对象中包含了比较复杂的数据结构。此时通过简单的赋值,并不能满足这种需求。
我之前做过一个需求:
定时任务——自动新建XX产品:XX产品包含了很多信息,有
基本类型数据
也有复杂结构对象
。已经上线的产品通过修改某些属性值就可以初始化形成新的XX产品,然后等待上线。这里就用到了克隆。
克隆的实现方式
浅度克隆
首先,定义一个Student
类,包含两成员变量:name
,age
,并且实现Cloneable
接口,代码如下:
public class Student implements Cloneable{
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
再,定义一个Teacher
类,也是两字段:一个name
,一个聚合对象student
,并且实现Cloneable
接口,代码如下:
public class Teacher implements Cloneable {
private String name;
private Student student;
public Teacher(String name, Student student) {
super();
this.name = name;
this.student = student;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
需要clone
的类为什么要实现Cloneable
接口?我们先看看该接口的源码:
package java.lang;
/**
* A class implements the <code>Cloneable</code> interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* <code>Cloneable</code> interface results in the exception
* <code>CloneNotSupportedException</code> being thrown.
* <p>
* By convention, classes that implement this interface should override
* <tt>Object.clone</tt> (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since JDK1.0
*/
public interface Cloneable {
}
这是一个“标示接口”
,即:没有任何方法和属性的接口。这个标示仅针对java.lang.Object#clone()
方法。注释中:
第一段可以看到:我们调用的clone()
方法是Obejct
类的方法;
第二段中:若调用这个Object.clone()
方法,但是不实现Cloneable
接口(not implement Cloneable)的话,会抛CloneNotSupportedException
异常。
我们来测试下,修改Student
方法,去掉实现。代码修改如下:
public class Student {
...
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student("jack", 27);
Student cloneStudent = (Student) student.clone();
}
}
执行结果如下:
Exception in thread "main" java.lang.CloneNotSupportedException: clone.Student
at java.lang.Object.clone(Native Method)
at clone.Student.main(Student.java:38)
第三段中说:按照惯例,对于Object.clone()
方法,我们需要是重写。我们看下该方法源码:
protected native Object clone() throws CloneNotSupportedException;
是个native
方法,一般来说native方法的效率要高于非native方法,因此比那种new出新对象再把旧对象的信息赋值到新对象的效率要高。该方法还是个protected
方法,也就是说外部程序想调用有局限性,因此需要重写修饰符设置为public
。
下面再看一个Teacher
相关的例子,这里先重写下equals()
方法:
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj instanceof Teacher) {
Teacher teacher = (Teacher) obj;
if (teacher.name == this.name && teacher.student.equals(this.student)) {
return true;
}
return false;
}
return false;
}
作如下测试:
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student("jack", 27);
Teacher teacher = new Teacher("Ali", student);
System.out.println("Teacher的内存地址:" + teacher);
Teacher cloneTeacher = (Teacher) teacher.clone();
System.out.println("克隆Teacher的内存地址:" + cloneTeacher);
System.out.println("克隆前后,Teacher对象是否相等:" + teacher.equals(cloneTeacher) + "\n");
System.out.println("++++++我只想修改克隆对象中的Student姓名为‘rose’++++++\n");
cloneTeacher.getStudent().setName("rose");
System.out.println("修改Student姓名后,Teacher的内存地址:" + teacher);
System.out.println("修改Student姓名后,克隆Teacher的内存地址:" + cloneTeacher);
System.out.println("修改Student姓名后,克隆前后,Teacher对象是否相等:" + teacher.equals(cloneTeacher));
}
执行结果如下:
Teacher的内存地址:clone.Teacher@6c89db9a
克隆Teacher的内存地址:clone.Teacher@4eb09321
克隆前后,Teacher对象是否相等:true
++++++我只想修改克隆对象中的Student姓名为‘rose’++++++
修改Student姓名后,Teacher的内存地址:clone.Teacher@6c89db9a
修改Student姓名后,克隆Teacher的内存地址:clone.Teacher@4eb09321
修改Student姓名后,克隆前后,Teacher对象是否相等:true
我们可以看到:
- 克隆前后,内存地址发生了变化
@6c89db9a
=>@4eb09321
; - 克隆前后以及修改了
Student
对象name
属性值,对象没有变化,equals
结果为true
。
也就是说,不同的引用指向了同一个对象。因此,若我只想修改克隆对象的信息,这种情况下是做不到的。
但是若只是修改基本类型
或者String字符串
,却能满足需求,只会影响克隆后的对象。譬如:
去掉:
cloneTeacher.getStudent().setName("rose");
并增加:
cloneTeacher.setName("Alice");
equals
结果:
修改Student姓名后,克隆前后,Teacher对象是否相等:false
最后别忘了重写下clone
方法,修改修饰符为public
:
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
深度克隆
深度克隆之所以有深度
,是弥补了浅度克隆对于对象类型的属性克隆的不足。这里通过对象序列化和反序列化来实现深度克隆。
对象的序列化
把对象转换为字节序列的过程
对象的反序列化:
把字节序列恢复为对象的过程
如何实现:
首先新建一个工具类,封装clone
方法:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class CloneUtil {
private CloneUtil() {}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
bais = new ByteArrayInputStream(baos.toByteArray());
ois = new ObjectInputStream(bais);
return (T) ois.readObject();
}
catch (ClassNotFoundException e) {
throw new RuntimeException("Class not found.", e);
}
catch (IOException e) {
throw new RuntimeException("Clone Object failed in IO.", e);
}
finally {
try {
if (ois != null) {
ois.close();
}
if (oos != null) {
oos.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
说明:
调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义。这两个基于内存的流就能够释只要垃圾回收器清理对象放资源,这一点不同于对外部资源(如文件流)的释放。
修改Student
代码如下:
public class Student implements Serializable{
private static final long serialVersionUID = 1L;
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
...
}
修改Teacher
代码如下:
public class Teacher implements Serializable {
private static final long serialVersionUID = 1L;
@Override
public String toString() {
return "Teacher [name=" + name + ", student=" + student + "]";
}
...
}
替换main
方法中测试代码:
Teacher cloneTeacher = (Teacher) teacher.clone();
为序列化-反序列化
克隆方法:
Teacher cloneTeacher = CloneUtil.clone(teacher);
执行结果:
Teacher的toString:Teacher [name=Ali, student=Student [name=jack, age=27]]
克隆Teacher的toString:Teacher [name=Ali, student=Student [name=jack, age=27]]
克隆前后,Teacher对象是否相等:false
++++++我只想修改克隆对象中的Student姓名为‘rose’++++++
修改Student姓名后,Teacher的的toString:Teacher [name=Ali, student=Student [name=jack, age=27]]
修改Student姓名后,克隆Teacher的的toString:Teacher [name=Ali, student=Student [name=rose, age=27]]
修改Student姓名后,克隆前后,Teacher对象是否相等:false
明显可以看到,修改Student
属性后,对克隆前的对象没有影响。
需要注意:CloneUtil
类中的clone()
长这样:
public static <T extends Serializable> T clone(T obj) {
...
}
基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型<T extends Serializable>
限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object
类的clone
方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
当然,若是聚合对象如Teacher
中的Student
没有实现Serializable
接口,还是会在运行时抛异常的。