神奇克隆术——Java深、浅Clone

	在我们日常编程的过程中,经常会遇到A模块向B模块请求获得一个数值或者对象的情况。然后,众所周知,Java中传递
数据的方式,分为值传递和引用传递(地址传递)。值传递自然很安全,但是引用传递(地址传递)的时候,A模块可能会
对从B模块中获取的对象(或者其他引用传递(地址传递)类型数据)进行恶意修改,从而影响B模块的运行,甚至导致B模块
的崩溃,这时候,使用神奇克隆术——Java深、浅Clone,就可以解决这个令人头疼的问题。

###一、事故场景再现

任何没有实际应用价值的知识,都是耍流氓,那么我们就来看看Java中的Clone究竟能在什么情景下起到奇妙的作用吧。

假设有这样一种情景:如果你正在开发一个银行管理系统,其中有一个功能是在客户端(Client)查看某人的账户余额,你采用简单工厂模式,由AccountFactory负责根据用户传入的用户名创建用户账号(PersonalAccount)的对象,然后返回给客户端,具体代码如下:

Client(客户端):

	public static void main(String[] args) {
		String name = "Li Ming";
		AccountFactory accountFactory = new AccountFactory();
		PersonalAccount personalAccount = accountFactory.getPersonalAccount(name);
		System.out.println(name +"的账户余额是" + personalAccount.getAccount());
	}

AccountFactory(账号工厂):

public class AccountFactory {
	PersonalAccount tempPersonalAccount = null;

	public PersonalAccount getPersonalAccount(String name) {
		if (tempPersonalAccount == null) {
			tempPersonalAccount = new PersonalAccount();
			tempPersonalAccount.setName(name);
			tempPersonalAccount.setAccount(10000);
			tempPersonalAccount.setAddress("南京");
		}
		return tempPersonalAccount;
	}
}

PersonalAccount(账号类):

public class PersonalAccount {
	private int account;//账户余额
	private String name;//账户名
	private String address;//账户拥有者地址

	public int getAccount() {
		return account;
	}
	public void setAccount(int account) {
		this.account = account;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}

}

上面这三段代码都很简单,就不详细介绍了。现在,我们来运行一下,我们惊喜地发现运行结果为:Li Ming的账户余额是10000。运行成功,没有任何错误,就目前状况来看,代码很完美啊。

**可是,事情往往没有你想象的那么简单。**下面,我们来在客户端搞点小破坏。假如,我就是Li Ming,一查账户余额,Oh,My God!怎么只剩1万了,一定是上个月网购花太多了。。。。怎么办呢?灵机一动,有了,我们在客户端加几行代码吧,改动后的客户端如下:

public static void main(String[] args) {
		String name = "Li Ming";
		AccountFactory accountFactory = new AccountFactory();
		PersonalAccount personalAccount = accountFactory.getPersonalAccount(name);
		System.out.println(name +"的账户余额是" + personalAccount.getAccount());
		personalAccount.setAccount(100000);//添加的代码。
		//重新查询我的账户余额。
		personalAccount = accountFactory.getPersonalAccount(name);
		System.out.println(name + "的账户余额(改过后的)是" + personalAccount.getAccount());
	}

我们再来运行一下,发现结果如下:

Li Ming的账户余额是10000
Li Ming的账户余额(改过后的)是100000

只改动一行,就变成了10万块,nice啊!但是,我知道,如果你是银行你肯定忍不了这样的事情发生,那究竟怎么避免和解决呢?之所以会产生这样的错误,是因为我们返回给客户端的PersonalAccount对象是引用类型的数据,客户端的改变会导致真实的数据改变。我们知道如果能够像值传递一样,不传递地址(引用),就可以避免这样的错误。那么究竟如何做呢?这里,就要用到我们的神奇克隆术——Java深、浅Clone。

###二、Java浅Clone

我们首先来看一下,使用Java的Clone机制如何改善我们的系统,具体的代码改变如下:

PersonalAccount(账号类):

public class PersonalAccount implements Cloneable{
	private int account;
	private String name;
	private String address;
	//增加Clone方法
	public Object clone(){
		PersonalAccount personalAccount = null;
		try {
			personalAccount = (PersonalAccount)super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return personalAccount;
	}
	//省略了一系列的get,set方法。
}

AccountFactory(账号工厂类):

public class AccountFactory {
	PersonalAccount tempPersonalAccount = null;

	public PersonalAccount getPersonalAccount(String name) {
		if (tempPersonalAccount == null) {
			tempPersonalAccount = new PersonalAccount();
			tempPersonalAccount.setName(name);
			tempPersonalAccount.setAccount(10000);
			tempPersonalAccount.setAddress("南京");
		}
		return (PersonalAccount)tempPersonalAccount.clone();//改动的地方。
	}
}

上面两个类中,PersonalAccount类增加了一个方法,实现了一个接口。AccountFactory中的返回语句发生了变动。改动很简单,我们再来运行一下我们的程序,我们发现,现在的输出为:

Li Ming的账户余额是10000
Li Ming的账户余额(改过后的)是10000

我们看到:此时,用户是无法通过客户端来改变用户的账户余额的,我们已经成功解决了第一个麻烦。下面,我们就来解释一下,我们到底是如何解决的。

首先,我们来看一下改动比较大的PersonalAccount类。

首先,我们可以看到PersonalAccount类implements了一个Cloneable接口,那么这个接口究竟做了什么呢?我们来看一下Java源码,如下:

**PS:如果你现在还不知道如何查看Java源码的话,请参考我的博客:**http://blog.csdn.net/qiumengchen12/article/details/44860867

这里写图片描述

我们从上图可以看到,Cloneable接口中其实一个方法也没有,其实这个接口只是一个标志,声明implements Cloneable的类只是表示这个类实现了Clone功能,那Clone功能到底是怎么实现的呢?我们看到PersonalAccount类中调用了super.clone()方法,我们知道所有的类都默认继承了java.lang.Object类,所以,我们去看看这个父类的clone方法是如何实现的。看一下Java源码,如下:

这里写图片描述

从上图,我们可以获得三个问题的答案。

**第一个,**我们看到clone()方法的访问权限为protected。(PS:如果你对Java修饰符不了解,请参照我的博客:http://blog.csdn.net/qiumengchen12/article/details/44939929),这就是我们为什么要在子类中覆盖重写父类clone方法,并且将访问修饰符改为public的原因,因为只有这样才能实现包外非子类的自由访问,当然了,如果你想把自己的方法限制为包内或子类访问的话,可以不改变修饰符。

**第二个,**我们看到clone()方法依旧没有方法体,但是我们发现clone()为native方法,native是什么意思呢?这个嘛,其实还是有很多讲究的,如果大概解释一下的话,那就是:native方法表示这个方法是有非Java语言来编写的,大多数为本地方法,大多数与操作系统等底层实现有关,当然了,效率也就比普通的方法高很多。其实,这也就是我们为什么要调用父类的clone方法(super.clone),而不是自己new一个对象,然后进行依次按项赋值来完成一个对象的拷贝。调用父类的clone方法不仅效率较高,而且操作简单,特别是在对象属性较多的情况时,更加明显。
PS:至于native方法以及Object中方法解析,我后续会写一个博客来详细分析,敬请期待。

**第三个,**我们看到该方法会抛出一个异常CloneNotSupportedException,我们看到上面的方法注释中说明了,如果我们没有声明implements Cloneable接口的话,就会抛出这个异常,其实,这也是我们为什么要声明implements Cloneable接口,即使它其实一个方法也没有。

到这里,我们基本解决并解释了第一个问题。如果你觉得到这里就万事大吉了。

那我只能说,事情往往没有你想象的那么简单。

我们再来搞点破坏。我们知道,正常情况下,我们会有一个单独的User类来负责管理存储用户信息,其实PersonalAccount类中,应该持有一个User类的对象,通过这个对象获取用户信息,而不是直接将name,address等用户信息直接存储在PersonalAccount类中,特别是那些和Account无关的用户信息等。那么,我们来改造一下我们的系统。

User类代码:

public class User {
	private String name;
	private String address;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
	
}

PersonalAccount类代码(主要增加了User属性,删除name,address属性):

public class PersonalAccount implements Cloneable{
	private int account;
	private User user;
	//增加Clone方法
	public Object clone(){
		PersonalAccount personalAccount = null;
		try {
			personalAccount = (PersonalAccount)super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return personalAccount;
	}
	
	public User getUser() {
		return user;
	}
	public void setUser(User user) {
		this.user = user;
	}
	public int getAccount() {
		return account;
	}
	public void setAccount(int account) {
		this.account = account;
	}

}

AccountFactory类代码(改变name赋值为生成User对象):

public class AccountFactory {
	PersonalAccount tempPersonalAccount = null;

	public PersonalAccount getPersonalAccount(String name) {
		if (tempPersonalAccount == null) {
			tempPersonalAccount = new PersonalAccount();
			tempPersonalAccount.setAccount(10000);
			//生成User对象
			User user = new User();
			user.setName(name);
			user.setAddress(name + "的家庭住址");
			tempPersonalAccount.setUser(user);
		}
		return (PersonalAccount)tempPersonalAccount.clone();
	}
}

客户端(Client)代码:

	public static void main(String[] args) {
		String name = "Li Ming";
		AccountFactory accountFactory = new AccountFactory();
		PersonalAccount personalAccount = accountFactory.getPersonalAccount(name);
		//修改后的代码
		User user = personalAccount.getUser();
		System.out.println(user.getName() + "\n" +user.getAddress() + "\n" +"账户余额:" + personalAccount.getAccount());
		System.out.println("*****************************************************");
		//尝试修改账户余额
		personalAccount.setAccount(100000);
		//再调用查询功能
		PersonalAccount tempPersonalAccount = accountFactory.getPersonalAccount(name);
		User user1 = tempPersonalAccount.getUser();
		System.out.println(user1.getName() + "\n" +user1.getAddress() + "\n" +"账户余额:" + tempPersonalAccount.getAccount());
		System.out.println("*****************************************************");
		//尝试修改用户信息类User
		user1.setName("我自己");
		user1.setAddress("我自己的住址");
		//再调用查询功能
		PersonalAccount tempPersonalAccount2 = accountFactory.getPersonalAccount(name);
		User user2 = tempPersonalAccount2.getUser();
		System.out.println(user2.getName() + "\n" +user2.getAddress() + "\n" +"账户余额:" + tempPersonalAccount2.getAccount());		
	}

以上的全部代码主要展示了我们增加User类之后的系统,我们来运行一下,发现结果如下:

Li Ming
Li Ming的家庭住址
账户余额:10000


Li Ming
Li Ming的家庭住址
账户余额:10000


我自己
我自己的住址
账户余额:10000

从上面的结果可以看出,我们加入User类之后,依旧能够防止对账户余额(Account)的修改,但是,这种情况下,竟然可以修改用户信息(User),将别人的账户改为自己的用户,据为己有!这,肯定忍不了!

之所以会产生上面的结果,是因为我们这里采用的是Java的浅Clone,如果我们采用Java的深Clone的话,会在一定程度上避免上面的错误产生。那么究竟什么是Java浅Clone,什么是Java深Clone,它们又有着怎样的区别呢?我们继续往下看。

###三、Java深Clone

在解释Java深、浅Clone的区别前,我们先来解释一下Java中Clone方法的底层实现机制。由于clone方法类型为native,我们并不能看到它的具体代码实现,那么它的底层究竟是如何实现的呢?

其实,是这样的:在执行clone操作的时候,底层会申请出一块和原来对象所占空间一样大小的存储空间,然后将原来对象所占空间的所有数据都原样拷贝到新申请到的空间,这样就获得了一个和原来一模一样的对象。

但是,我们需要注意的是,在拷贝过程中,值类型的数据,当然没有问题,比如int型,String型(比较特殊)等其他的基本数据类型。但,对于引用类型的数据,如对象等。在原来的地址空间中存储的就是一个指向真实对象的引用,拷贝到新的地址空间之后,引用还是指向同一个真实对象。如下图:

这里写图片描述

那么现在我们对Clone得到的对象中的Account等值类型数据进行更改的时候,并不会影响原有的对象中的数据,但是,当我们对Clone得到的对象中的User等引用类型数据进行更改的时候,因为指向的是同一个真实对象,那么就一定会影响到原有的对象中的数据。这就是所谓的浅Clone

那么,我们如何避免这种错误呢,我想有的人可能已经想到了解决方法,那就是在对PersonalAccount对象进行clone操作时,对其中的User对象等所有引用类型的数据也同样进行一次clone操作,当然了,这样操作的前提是User类能够像PersonalAccount类一样implements Cloneable接口,实现并重写父类的clone方法。这就是深Clone。

当然了,正如你们所想,如果User类中还持有其他引用数据类型的数据,那么对于这些数据,也必须采用同样的深Clone操作,也就是说所有存在于引用链中的所有引用类型数据,都必须进行深Clone操作。你可能会说,这要是很多层引用嵌套,岂不是会呈现爆炸式地复杂度增加,不可否认,确实是这样的。也正是因为这个原因,所以一般在引用嵌套层数较少的情况下才使用深Clone,在嵌套层数过多时,往往会导致复杂度过高而无法使用深度Clone。此时,也许会因为在复杂度和深度Clone之间的权衡中,做出一种“不伦不类”的做法,那就是一部分引用类型数据使用clone操作,一部分引用类型数据直接使用new并依次按项赋值的方式进行手动Clone。当然,不用说,也知道这是一种不太好的做法。

那么我们应该如何应对嵌套层数较多,数据信息内容较复杂的对象的Clone操作呢?也许采用序列化的方式,是一个不错的选择,那么如何通过序列化方式进行相关操作呢?后续的博客中,我会详细介绍,由于篇幅限制,这里就不展开了。

下面,我们来看一下,就目前我们的状况:存在简单嵌套的情况下,我们应该如何进行深度Clone呢。

我们先来看一下代码的修改:

User类代码改动(增加Cloneable接口,重写父类clone方法):

public class User implements Cloneable{
	private String name;
	private String address;

	public Object clone(){
		User user = null;
		try {
			user = (User)super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return user;
	}
	//省略一系列get,set方法。
}

PersonalAccount类代码改动(只是修改clone方法):

	public Object clone(){
		PersonalAccount personalAccount = null;
		try {
			personalAccount = (PersonalAccount)super.clone();
			User user = (User)personalAccount.getUser().clone();
			personalAccount.setUser(user);
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return personalAccount;
	}

此时,深Clone的操作情况如下:

这里写图片描述

上面两个类的改动很小,也很简单,就不解释了。其他类都不发生变化,此时,我们再来运行一下客户端的代码,我们发现,运行结果如下:

Li Ming
Li Ming的家庭住址
账户余额:10000


Li Ming
Li Ming的家庭住址
账户余额:10000


Li Ming
Li Ming的家庭住址
账户余额:10000

我们发现,采用深Clone之后,无论是值类型数据(如:Account)还是引用类型数据(user),都无法在客户端进行修改。到这里,我们的系统比一开始的原始系统要安全健壮多了。到这里呢,我们的深、浅Clone,也算基本讲完了。

###四、最后的思考

在这篇博客中,我们在不断地提高一个银行系统的巩固性,安全性的过程中,详细地分析学习了Java中的深、浅Clone的知识。而且,我们清晰地认识到,深Clone虽然能够解决引用类型数据的Clone问题,但是在引用嵌套层数较多时,较高的复杂度使我们很难通过深Clone的方式进行对象克隆操作,甚至是无法正确进行深Clone操作,而那种“不伦不类”的方法虽然可以一定程度上缓解复杂度的问题,但是终究是不够美感。这时,也许通过Java序列化,可以曲线救国。我们在后续的博客中,将详细介绍,如何巧妙地使用Java序列化完成对象克隆操作等。

如果你有任何疑问和建议,欢迎给我留言,希望我们在讨论中共同进步。

PS:源码下载地址:百度网盘:链接:http://pan.baidu.com/s/1o6JtiKq 密码:yo29

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值