java对象的浅拷贝和深拷贝

     我们知道,每个对象都有拷贝其对象的能力,是因为每个对象都是一个Object子类,而Object提供clone方法,一个类实现了Cloneable接口就表示该类具备了被拷贝的能力,如果再覆写里面的clone方法就会完全具备拷贝的能力,拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是在大对象的生成上,这会使性能的提升非常显著,Object提供的clone方法只是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,其拷贝规则如下:

基本类型:则拷贝其值

对象:拷贝地址引用,也就是说新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。

String字符串:拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String pool)中重新生成新的字符串,原有的字符串对象保持不变,在此我们可以认为String是一个基本类型。

下面我们来看一个实例:

定义Person类,让其实现Cloneable接口,并在其中添加相应的属性(名称和父亲)和覆写Object中的clone方法:

package com.xin.suggestion;

public class Person implements Cloneable{
	private String name;
	private Person father;
	
	public Person(String name){
		this.name=name;
	}
	
	public Person(String name,Person father){
		this.name=name;
		this.father=father;
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Person getFather() {
		return father;
	}
	public void setFather(Person father) {
		this.father = father;
	}
	
	//覆写Object中的clone方法
	@Override
	protected Person clone(){
		Person p=null;
		try {
			p=(Person)super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return p;
	}
	
	public static void main(String[] args) {
		Person father=new Person("父亲");
		Person son1=new Person("大儿子",father);
		//小儿子和大儿子拥有相同的属性,所以从大儿子进行拷贝
		Person son2=son1.clone(); 
		son2.setName("小儿子");
		System.out.println(son1.getName()+" 的父亲是:"+son1.getFather().getName());
		System.out.println(son2.getName()+" 的父亲是:"+son2.getFather().getName());
	}
}

得出结果如下:

大儿子 的父亲是:父亲
小儿子 的父亲是:父亲
下面我们来看看其拷贝过程:

拷贝之前,大儿子:

拷贝之后,小儿子通过setName修改了其名字:

小儿子拷贝大儿子后,对于father,只拷贝了其引用,所以得到上面的结果;

如果大儿子要认个干爹,我们修改大儿子的父亲的名称,将其改为干爹:

	public static void main(String[] args) {
		Person father=new Person("父亲");
		Person son1=new Person("大儿子",father);
		//小儿子和大儿子拥有相同的属性,所以从大儿子进行拷贝
		Person son2=son1.clone(); 
		son2.setName("小儿子");
		//大儿子认了个干爹,修改其父亲名称
		son1.getFather().setName("干爹");
		System.out.println(son1.getName()+" 的父亲是:"+son1.getFather().getName());
		System.out.println(son2.getName()+" 的父亲是:"+son2.getFather().getName());
	}

打印结果:

大儿子 的父亲是:干爹
小儿子 的父亲是:干爹

这下可好了,大儿子认了干爹,小儿子也得认干爹,这个父亲的两个儿子都认干爹去了,这是因为我们只修改了父亲的名称,而两个儿子对其的引用没有变化,如下图所示:

所以会出现上面的结果,上面中的clone方法只是实现了浅拷贝,而实际上我们只需要大儿子认干爹,而小儿子还是自己的儿子,所以真正的深拷贝是小儿子里面的所有的东西都从大儿子那里拷贝一份,修改clone方法:

	//覆写Object中的clone方法
	@Override
	protected Person clone(){
		Person p=null;
		try {
			p=(Person)super.clone();
			//父亲不只是拷贝一份引用,而是全部拷贝一份
			p.setFather(new Person(p.getFather().getName()));
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return p;
	}

这样我们测试上面的结果,得到:

大儿子 的父亲是:干爹
小儿子 的父亲是:父亲
其深度拷贝方式图解如下:

此深拷贝方式使大儿子和小儿子都有一份引用指向各自的父亲,而不是只拷贝引用指向父亲,这里就实现了我们的深度拷贝,浅拷贝只是java提供的一种简单的拷贝机制,不方便我们直接使用。

我们在进行对象拷贝的时候,推荐使用对象序列化的方式进行拷贝,在内存中通过字节流拷贝的方式来实现,就是把母对象写入一个字节流中,再从字节流中将其读取出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个新对象,建议使用此种方式,下面是进行拷贝的一个工具类:

public class CloneUtils {
	
	@SuppressWarnings("unchecked")
	public static <T extends Serializable> T clone(T obj){
		//拷贝产生的对象
		T cloneObj=null;
		try {
			//读取对象字节数据
			ByteArrayOutputStream baos=new ByteArrayOutputStream();
			ObjectOutputStream oos=new ObjectOutputStream(baos);
			oos.writeObject(obj);
			oos.close();
			//分配内存空间,写入原始对象,生成新对象
			ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
			ObjectInputStream ois=new ObjectInputStream(bais);
			//返回新对象,并做类型装换
			cloneObj=(T)ois.readObject();
			ois.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return cloneObj;
	}
}

 测试依靠序列化的方式进行深度拷贝:

package com.xin.suggestion;

import java.io.Serializable;

public class Person2 implements Serializable{

	/**
	 * 
	 */
	private static final long serialVersionUID = 4389118360256841607L;
	
	private String name;
	private Person2 father;
	
	public Person2(String name){
		this.name=name;
	}
	
	public Person2(String name,Person2 father){
		this.name=name;
		this.father=father;
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Person2 getFather() {
		return father;
	}
	public void setFather(Person2 father) {
		this.father = father;
	}
	
	public static void main(String[] args) {
		Person2 father=new Person2("父亲");
		Person2 son1=new Person2("大儿子",father);
		//使用序列化的方式进行深度拷贝
		Person2 son2=CloneUtils.clone(son1);
		son1.getFather().setName("干爹");
		son2.setName("小儿子");
		System.out.println(son1.getName()+" 的父亲是:"+son1.getFather().getName());
		System.out.println(son2.getName()+" 的父亲是:"+son2.getFather().getName());
	}
}

输出结果:

大儿子 的父亲是:干爹
小儿子 的父亲是:父亲
使用此方式进行拷贝,需要注意:
1、对象的内部属性都是可序列化的,如果有内部属性不可序列化,则会抛出序列化异常
2、注意方法和属性的特殊修饰符:比如final、static变量的序列化问题会被引入到对象拷贝中来,这点需要特别注意,同时transient变量(瞬态变量,不进行序列化的变量)也会影响拷贝效果
例如在上面中的father属性,将其添加transient关键字:private transient Person2 father;那么运行结果将会出现NullPointException,所以要慎用。

采用序列化方式拷贝时,还有一个更简单的办法,就是使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便,大家可以自行研究一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值