引子
为啥要用clone方法?
最近在项目中发现某开发人员代码有问题,然而单元测试也确实不通过,就是对对象的引用失败造成的
具体如下:
在对某个对象更新保存数据操作,对象关联某个文件需要将对象更新到数据库后再判断文件是否更新(文件存储到专门的文件系统中,对象保持文件的访问路径),如果文件更新了,那么就需要上传对象原来的文件,因此需要对要更新的对象保留一份副本
然而再代码审查的时候,发现小哥哥这样写的:
上图中错误的写法已经被注释了,就是使用“另一个”对象oldRecord,用temp赋值给它;
希望用oldRecord保存住一个temp的副本,然后这是大大的错误,后面的使用中就会发现,oldRecord并不能保留住temp的副本,原因就是oldRecord跟temp完全就是相同的一个对象,oldRecord的指向就是temp的引用
在java中,对像temp已经存在内存中,它的所有数据位于java堆中,我们所说的temp也是指向数据所在的堆的地址,比如说temp此时位于 0x32457D20(内存地址)这个地址下,当把oldRecord = temp;这种赋值,那么对象oldRecord的指向地址仍然是0x32457D20,与temp完全相同,不论是对oldRecord或者temp的修改均是修改了地址 0x32457D20 下面的数据,而 0x32457D20(内存)地址是永远不变的。
上面图中给出了一种修改方法,就是用temp.clone()创建一个对象副本,使用了clone方法就相当于新new了一个对象,显然新new的对象的地址不可能是0x32457D20(因为这个地址已经被temp或者oldRecord占用了)
当然了,知道了上面的原理,如果不用clone方法,直接new一个SubSystemDisplay对象出来,然后将temp的每个属性再分别赋值给新new的对象也行,不过这就显得有点麻烦了。
下面主要讲讲java的clone()方法
浅复制与深复制概念
-
浅复制(浅克隆)
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。 -
深复制(深克隆)
被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
Java的clone()方法
- clone方法将对象复制了一份并返回给调用者。一般而言,clone() 方法满足
- 对任何的对象x,都有x.clone() !=x //克隆对象与原对象不是同一个对象
- 对任何的对象x,都有x.clone().getClass() == x.getClass() //克隆对象与原对象的类型一样
- 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立
- Java中对象的克隆
- 为了获取对象的一份拷贝,我们可以利用Object类的clone()方法
- 在派生类中覆盖基类的clone()方法,并声明为public
- 在派生类的clone()方法中,调用super.clone()
- 在派生类中实现Cloneable接口
请看如下代码:
public class SubSystemDisplay extends BaseModel implements Cloneable {
private static final long serialVersionUID = 1L;
private String sub_code;// 子系统表主键sub_code
private String data_id;// 数据字段映射表dtjx_transmissioncode主键id
public SubSystemDisplay() {
}
public SubSystemDisplay(String sub_code){
this.sub_code = sub_code;
}
public SubSystemDisplay(String sub_code, String data_id){
this.sub_code = sub_code;
this.data_id = data_id;
}
public String getSub_code() {
return sub_code;
}
public void setSub_code(String sub_code) {
this.sub_code = sub_code;
}
public String getData_id() {
return data_id;
}
public void setData_id(String data_id) {
this.data_id = data_id;
}
@Override
public SubSystemDisplay clone() {
SubSystemDisplay o = null;
try {
o = (SubSystemDisplay) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
说明:
- 为什么我们在派生类中覆盖Object的clone()方法时,一定要调用super.clone()呢?在运行时刻,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中。
- 继承自java.lang.Object类的clone()方法是浅复制。这是显然的,根据引子里面的java对象底层原理就知道,一个对象存在只要其引用的地址不改变再怎么引用还是指向这个对象
那应该如何实现深层次的克隆?
深克隆
简而言之就是对象中对其对象再使用clone()
下面用代码说明:
package com.june.clone;
import java.io.Serializable;
/**
* 抽象类--人
* @author junehappylove
*
*/
public abstract class Person implements Serializable{
private static final long serialVersionUID = 19880316L;
private String name;
private int 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;
}
}
package com.june.clone;
/**
* 教师类
* @author junehappylove
*
*/
public class Teacher extends Person implements Cloneable {
private static final long serialVersionUID = 19880316L;
public Teacher(String name, int age){
super();
super.setName(name);
super.setAge(age);
}
@Override
public Object clone(){
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
package com.june.clone;
/**
* 学生类
* @author junehappylove
*
*/
public class Student extends Person implements Cloneable {
private static final long serialVersionUID = 19880316L;
private Teacher teacher;//学生的老师
public Student(String name, int age, Teacher teacher){
super();
super.setName(name);
super.setAge(age);
this.teacher = teacher;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public Object clone(){
Student o = null;
try {
o = (Student)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}//上面已经可以做到浅层克隆了,深层克隆就需要解决学生的老师对象
o.setTeacher((Teacher)getTeacher().clone());//这里设置一下教师的克隆对象即可
return o;
}
}
package com.june.clone;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* 对学生类的测试
* @author junehappylove
*
*/
public class StudentTest {
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test//测试Student/Teacher的深层克隆方法
public void testClone() {
Teacher june = new Teacher("june", 30);
Student xiaoming = new Student("xiaoming", 15, june);
Student xiaohong = (Student) xiaoming.clone();//小红是由小明克隆的
assertFalse(xiaoming.getTeacher().equals(xiaohong.getTeacher()));//此时两个学生的老师可不是同一个人,虽然名字和年龄都相同,注意了!
assertFalse(xiaoming.getTeacher() == xiaohong.getTeacher());//相等方法不用 == 判断,这两个对象的引用地址明显是不同的
// 假设学生小红的教师变了
Teacher judy = new Teacher("judy", 28);
xiaohong.setTeacher(judy);
// 而小红使用小明克隆的,那么小明的老师是否也跟着变化了呢?
// 如果小明的老师也变了,那么说明学生Student这个类的clone方法是浅复制的
// 如果小明的老师仍然是june,那么说明Student这个类的clone方法是深复制的
// 这里参考Student的clone方法,可知学生类是一个深复制
assertFalse(xiaoming.getTeacher().equals(xiaohong.getTeacher()));
}
}
上面代码中对于深复制(克隆)会发现一个小瑕疵,就是对小红是从小明克隆而来,他俩什么都没做,然而他俩的老师june确是不一样的,这不合理啊,他们的老师名字相同年龄也相同,他们的老师应该是用一个人才对,除非对小红的老师重新设置了名称judy和年龄28,否则june正常情况下就是同一个人,但是测试中发现小红的老师和小明的老师june确不是同一个人!
啥情况?
注意了 这就是深克隆的一个不足之处,因为深克隆就是把对象的对象重新new了一遍。因此对深克隆,我们总是要“精心”的重写对象的equals
方法。
对于上面的Teacher类,我们可以重写equals方法,如下:
@Override//对于被深层克隆的对象总是需要重新equals方法
public boolean equals(Object obj) {
Teacher t = (Teacher)obj;
return t.getName().equals(this.getName()) && t.getAge() == this.getAge();
}
然而当你重写了equals
方法后,你总是还需要重写hashCode
方法,这就是另外需要讨论的了1。
利用序列化来做深复制
在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
如下为深复制源代码
public Object deepClone() {
//将对象写到流里
ByteArrayOutoutStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(this);
//从流里读出来
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi=new ObjectInputStream(bi);
return(oi.readObject());
}
这样做的前提是对象以及对象内部所有引用到的对象都是可序列化的,也就是类需要实现java.io.Serializable
接口,否则,就需要仔细考察那些不需要序列化的对象,将其设置成transient
,从而将之排除在复制过程之外。
总而言之,就是想办法new出来,而不是引用原对象的地址!
为啥要重写hashCode?请参考: https://blog.csdn.net/junehappylove/article/details/90901654 ↩︎