由赋值语句导入对象拷贝
在我们实际开发中,常常会用到简单的赋值语句=
就比如下面这个例子:
//赋值语句 基本数据类型
int a=3;
int b=a;
a=5;
System.out.println(b);
//赋值语句 String字符串
String s1="s1";
String s2=s1;
s1="s2";
System.out.println(s2);
打印结果如下
这个没什么可说的。
但是我们看下面这个例子:
class Person {
Person son;
String name;
public Person(Person son, String name) {
this.son = son;
this.name = name;
}
public Person(String name) {
this.name = name;
}
}
上面定义了一个普通的Person类
//对象赋值,hashCode一致
Person son = new Person("儿子jack");
Person p1 = new Person(son, "paul_1");
Person p2 = p1;
// p2=new Person(son,"paul_3");
p2.name = "paul_2";
p2.son=new Person("儿子seven");
System.out.println("p1.hashCode:" + p1.hashCode());
System.out.println("p2.hashCode:" + p2.hashCode());
System.out.println(p1.name);
System.out.println("p1.son.name:" + p1.son.name);
打印结果
p1.hashCode:21685669
p2.hashCode:21685669
paul_2
p2.son.name:儿子seven
这个时候我们就会有疑惑
1:为什么p1赋值给p2,修改p2内部的变量属性,会影响到p1的内部变量的属性
2:我们发现,赋值后,p1和p2的hashCode还是一样的,并没有创建一个新的对象
其实这就是所谓的对象拷贝,我们可以总结一下对象拷贝的特点:
首先我们需要知道:
Java 中的数据类型分为基本数据类型和引用数据类型。
对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,
会有值传递和引用(地址)传递的差别。
对象拷贝:
a=b
对于基本数据类型:属于值传递,修改一个不会影响另外一个
对于引用数据类型:是引用传递,修改a会影响b,修改b也会影响a。因为a和b实际是同一个内存地址的同一个对象
所以针对上面的代码,我们把注释 p2=new Person(son,“paul_3”);打开
结果就变成:
p1.hashCode:21685669
p2.hashCode:2133927002
paul_1
p1.son.name:儿子jack
这时候就正常了
因为p2其实已经指向了一个新的new出来的对象了
p2和p1指向的已经是互相没有关联的两个对象了
浅拷贝
那么如何解决刚刚上面的问题呢?
比如Person a=b;
我改动a的成员属性,不想影响到b,该如何做呢?
这就需要我们了解浅拷贝和深拷贝了
还是用一个例子来说明
我们对Person进行一个改造
让他实现Cloneable接口
然后重写他的toString和clone方法
class Person implements Cloneable{
Person son;
String name;
public Person(Person son, String name) {
this.son = son;
this.name = name;
}
public Person(String name) {
this.name = name;
}
/**
* 重写clone()方法
*
* @return
*/
@Override
public Object clone() {
//浅拷贝
try {
// 直接调用父类的clone()方法
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
@Override
public String toString() {
return "[Person_name: " + name + ", hashCode:" + this.hashCode() + ", son:" + son + "]";
}
}
再看我们的测试代码:
Person son = new Person("张三之子");
Person p1 = new Person(son, "张三");
Person p2 = (Person) p1.clone();
p2.name = "李四";
p2.son.name = "李四之子";
System.out.println(p1);
System.out.println(p2);
打印结果:
[Person_name: 张三, hashCode:21685669, son:[Person_name: 李四之子, hashCode:2133927002, son:null]]
[Person_name: 李四, hashCode:1836019240, son:[Person_name: 李四之子, hashCode:2133927002, son:null]]
这时候我们发现了:
1:p2修改自己的姓名,的确没有影响到p1,并且p1和p2的hashCode也是不同的
说明是两个对象
2:但是p2修改自己儿子的姓名,却影响到了p1儿子的姓名,并且p1和p2的son,
他们的hashCode都是一样的,说明是同一个对象
其实上面做的就是一个浅拷贝
步骤就是
1、实现类的Cloneable接口
2、重写类的clone方法
那么浅拷贝的特点是什么呢?
总结就是:
1:会对浅拷贝的对象本身做一个对象的复制
2:对于浅拷贝对象内的基本数据类型,还是值传递
3:对于浅拷贝对象内的引用数据类型,是值传递,就是说还是同一个对象
以上三点用这个图表示:
所以,浅拷贝只会复制对象本身,对于对象内部的引用数据类型的变量,是不会做拷贝的
自然修改p2的son的名字,还是会影响到p1的son的名字的
除非p2或者p1的son又指向一个新new出来的son
两者才不会互相影响
深拷贝
浅拷贝只能解决最外层对象的影响问题
如果要解决内部的互相影响问题,就需要深拷贝来处理了
首先我们对Person类做一个深拷贝的处理
(怎么处理文章后面会讲)
然后还是上面的测试代码,打印结果如下:
[Person_name: 张三, hashCode:21685669, son:[Person_name: 张三之子, hashCode:2133927002, son:null]]
[Person_name: 李四, hashCode:1836019240, son:[Person_name: 李四之子, hashCode:325040804, son:null]]
可以看到,这下问题彻底解决了
p1和p2是两个对象,p1和p2的son也是不同的两个对象了
他们的hashCode也都不一样
这就是深拷贝的特点了
1:对于基本数据类型,都是值传递,这点和浅拷贝是一样的
2:对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
3:深拷贝相比于浅拷贝速度较慢并且花销较大。
4:为什么开销大呢?因为对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
具体就像下图:
讲完了深拷贝的特点,我们看下深拷贝的两种实现方式:
第一种,就是对象内部的每个对象都实现 Cloneable 并重写 clone() 方法,
进而实现了对象的串行层层拷贝。
比如刚刚的Person要进行深拷贝,就应该这样处理:
class Person implements Cloneable{
Person son;
String name;
public Person(Person son, String name) {
this.son = son;
this.name = name;
}
public Person(String name) {
this.name = name;
}
/**
* 重写clone()方法
* @return
*/
@Override
public Object clone() {
//深拷贝
try {
// 直接调用父类的clone()方法
Person person = (Person) super.clone();
if (son!=null){
person.son = (Person) son.clone();
}
return person;
} catch (CloneNotSupportedException e) {
return null;
}
}
@Override
public String toString() {
return "[Person_name: " + name + ", hashCode:" + this.hashCode() + ", son:" + son + "]";
}
}
可以看到,对于Person内部的son,也做了拷贝处理
如果son不是Person类型的,而是其他引用类型
比如什么Woman类型
那么也要对Woman类型做这样的处理:
1、实现Cloneable接口
2、重写clone方法
可以看到,这种方式比较麻烦,写了很多重复的代码
所以我们也可以用第二种方式:
1:让Person类实现Serializable接口
2:通过字节流序列化实现深拷贝
具体代码如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* 通过字节流序列化实现深拷贝,需要深拷贝的对象必须实现Serializable接口
*
* @author Administrator
*/
public class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
T cloneObj = null;
try {
// 写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
// 分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
// 返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
使用起来就是这样:
Person son = new Person("张三之子");
Person p1 = new Person(son, "张三");
Person p2 = CloneUtils.clone(p1);
// Person p2 = (Person) p1.clone();
p2.name = "李四";
p2.son.name = "李四之子";
System.out.println(p1);
System.out.println(p2);
最终也实现了深拷贝
总结
不同的拷贝方式有不同的特点
实际开发中,不是说我们就一定要用深拷贝或者浅拷贝
而是根据自己的实际业务需求
所以只有了解清楚了不同拷贝的特点
并且能够灵活的使用这三种拷贝方式
才算是真正的掌握了。