我們知道在 Java 中存在這個(gè)接口 Cloneable,實(shí)現(xiàn)該接口的類都會(huì)具備被拷貝的能力,同時(shí)拷貝是在內(nèi)存中進(jìn)行,在性能方面比我們直接通過(guò) new 生成對(duì)象來(lái)的快,特別是在大對(duì)象的生成上,使得性能的提升非常明顯。然而我們知道拷貝分為深拷貝和淺拷貝之分,但是淺拷貝存在對(duì)象屬性拷貝不徹底問(wèn)題。關(guān)于深拷貝、淺拷貝的請(qǐng)參考這里:漸析 java 的淺拷貝和深拷貝
我們先看如下代碼:
public class Person implements Cloneable{
/** 姓名 **/
private String name;
/** 電子郵件 **/
private Email email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Email getEmail() {
return email;
}
public void setEmail(Email email) {
this.email = email;
}
public Person(String name,Email email){
this.name = name;
this.email = email;
}
public Person(String name){
this.name = name;
}
protected Person clone() {
Person person = null;
try {
person = (Person) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return person;
}
}
public class Client {
public static void main(String[] args) {
//寫封郵件
Email email = new Email("請(qǐng)參加會(huì)議","請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...");
Person person1 = new Person("張三",email);
Person person2 = person1.clone();
person2.setName("李四");
Person person3 = person1.clone();
person3.setName("王五");
System.out.println(person1.getName() + "的郵件內(nèi)容是:" + person1.getEmail().getContent());
System.out.println(person2.getName() + "的郵件內(nèi)容是:" + person2.getEmail().getContent());
System.out.println(person3.getName() + "的郵件內(nèi)容是:" + person3.getEmail().getContent());
}
}
--------------------
Output:
張三的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
李四的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
王五的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
在該應(yīng)用程序中,首先定義一封郵件,然后將該郵件發(fā)給張三、李四、王五三個(gè)人,由于他們是使用相同的郵件,并且僅有名字不同,所以使用張三該對(duì)象類拷貝李四、王五對(duì)象然后更改下名字即可。程序一直到這里都沒有錯(cuò),但是如果我們需要張三提前 30 分鐘到,即把郵件的內(nèi)容修改下:
public class Client {
public static void main(String[] args) {
//寫封郵件
Email email = new Email("請(qǐng)參加會(huì)議","請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...");
Person person1 = new Person("張三",email);
Person person2 = person1.clone();
person2.setName("李四");
Person person3 = person1.clone();
person3.setName("王五");
person1.getEmail().setContent("請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...");
System.out.println(person1.getName() + "的郵件內(nèi)容是:" + person1.getEmail().getContent());
System.out.println(person2.getName() + "的郵件內(nèi)容是:" + person2.getEmail().getContent());
System.out.println(person3.getName() + "的郵件內(nèi)容是:" + person3.getEmail().getContent());
}
}
在這里同樣是使用張三該對(duì)象實(shí)現(xiàn)對(duì)李四、王五拷貝,最后將張三的郵件內(nèi)容改變?yōu)椋赫?qǐng)與今天 12:00 到二會(huì)議室參加會(huì)議…。但是結(jié)果是:
張三的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
李四的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
王五的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
這里我們就疑惑了為什么李四和王五的郵件內(nèi)容也發(fā)送了改變呢?讓他們提前30分鐘到人家會(huì)有意見的!
其實(shí)出現(xiàn)問(wèn)題的關(guān)鍵就在于 clone() 方法上,我們知道該 clone() 方法是使用 Object 類的 clone() 方法,但是該方法存在一個(gè)缺陷,它并不會(huì)將對(duì)象的所有屬性全部拷貝過(guò)來(lái),而是有選擇性的拷貝,基本規(guī)則如下:
1、基本類型
如果變量是基本很類型,則拷貝其值,比如 int、float 等。
2、對(duì)象
如果變量是一個(gè)實(shí)例對(duì)象,則拷貝其地址引用,也就是說(shuō)此時(shí)新對(duì)象與原來(lái)對(duì)象是公用該實(shí)例變量。
3、String 字符串
若變量為 String 字符串,則拷貝其地址引用。但是在修改時(shí),它會(huì)從字符串池中重新生成一個(gè)新的字符串,原有紫都城對(duì)象保持不變。
基于上面上面的規(guī)則,我們很容易發(fā)現(xiàn)問(wèn)題的所在,他們?nèi)吖靡粋€(gè)對(duì)象,張三修改了該郵件內(nèi)容,則李四和王五也會(huì)修改,所以才會(huì)出現(xiàn)上面的情況。對(duì)于這種情況我們還是可以解決的,只需要在 clone() 方法里面新建一個(gè)對(duì)象,然后張三引用該對(duì)象即可:
protected Person clone() {
Person person = null;
try {
person = (Person) super.clone();
person.setEmail(new Email(person.getEmail().getObject(),person.getEmail().getContent()));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return person;
}
所以:淺拷貝只是 Java 提供的一種簡(jiǎn)單的拷貝機(jī)制,不便于直接使用。
對(duì)于上面的解決方案還是存在一個(gè)問(wèn)題,若我們系統(tǒng)中存在大量的對(duì)象是通過(guò)拷貝生成的,如果我們每一個(gè)類都寫一個(gè) clone() 方法,并將還需要進(jìn)行深拷貝,新建大量的對(duì)象,這個(gè)工程是非常大的,這里我們可以利用序列化來(lái)實(shí)現(xiàn)對(duì)象的拷貝。
如何利用序列化來(lái)完成對(duì)象的拷貝呢?在內(nèi)存中通過(guò)字節(jié)流的拷貝是比較容易實(shí)現(xiàn)的。把母對(duì)象寫入到一個(gè)字節(jié)流中,再?gòu)淖止?jié)流中將其讀出來(lái),這樣就可以創(chuàng)建一個(gè)新的對(duì)象了,并且該新對(duì)象與母對(duì)象之間并不存在引用共享的問(wèn)題,真正實(shí)現(xiàn)對(duì)象的深拷貝。
public class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj){
T cloneObj = null;
try {
//寫入字節(jié)流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配內(nèi)存,寫入原始對(duì)象,生成新對(duì)象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新對(duì)象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
使用該工具類的對(duì)象必須要實(shí)現(xiàn) Serializable 接口,否則是沒有辦法實(shí)現(xiàn)克隆的。
public class Person implements Serializable{
private static final long serialVersionUID = 2631590509760908280L;
..................
//去除clone()方法
}
public class Email implements Serializable{
private static final long serialVersionUID = 1267293988171991494L;
....................
}
所以使用該工具類的對(duì)象只要實(shí)現(xiàn) Serializable 接口就可實(shí)現(xiàn)對(duì)象的克隆,無(wú)須繼承 Cloneable 接口實(shí)現(xiàn) clone() 方法。
public class Client {
public static void main(String[] args) {
//寫封郵件
Email email = new Email("請(qǐng)參加會(huì)議","請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...");
Person person1 = new Person("張三",email);
Person person2 = CloneUtils.clone(person1);
person2.setName("李四");
Person person3 = CloneUtils.clone(person1);
person3.setName("王五");
person1.getEmail().setContent("請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...");
System.out.println(person1.getName() + "的郵件內(nèi)容是:" + person1.getEmail().getContent());
System.out.println(person2.getName() + "的郵件內(nèi)容是:" + person2.getEmail().getContent());
System.out.println(person3.getName() + "的郵件內(nèi)容是:" + person3.getEmail().getContent());
}
}
-------------------
Output:
張三的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
李四的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
王五的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
鞏固基礎(chǔ),提高技術(shù),不懼困難,攀登高峰?。。。。?!
參考文獻(xiàn)《編寫高質(zhì)量代碼 改善Java程序的151個(gè)建議》—-秦小波