Java 物件拷貝:clone方法 以及 反序列化
我們知道在Java中存在這個介面Cloneable,實現該介面的類都會具備被拷貝的能力,同時拷貝是在記憶體中進行,在效能方面比我們直接通過new生成物件來的快,特別是在大物件的生成上,使得效能的提升非常明顯。然而我們知道拷貝分為深拷貝和淺拷貝之分,但是淺拷貝存在物件屬性拷貝不徹底問題。
A:淺拷貝(淺克隆): 淺拷貝僅僅複製所考慮的物件,而不復制它所引用的物件。 b:深拷貝(深克隆):深拷貝把要複製的物件所引用的物件都複製了一遍。
一、clone方法淺拷貝問題:
Java中物件的克隆,為了獲取物件的一份拷貝,我們可以利用Object類的clone()方法。Object類裡的clone方法是淺拷貝。
必須要遵循下面三點: 1.在派生類中覆蓋基類的clone()方法,並宣告為public【Object類中的clone()方法為protected的】。 2.在派生類的clone()方法中,呼叫super.clone()。 3.在派生類中實現Cloneable介面。
先看以下程式碼:
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 Email { private Object name; private String content; public Email(Object name, String content) { this.name = name; this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Object getName() { return name; } public void setName(Object name) { this.name = name; } } public class Client { public static void main(String[] args) { //寫封郵件 Email email = new Email("請參加會議","請與今天12:30到二會議室參加會議..."); Person person1 = new Person("張三",email); Person person2 = person1.clone(); person2.setName("李四"); Person person3 = person1.clone(); person3.setName("王五"); System.out.println(person1.getName() + "的郵件內容是:" + person1.getEmail().getContent()); System.out.println(person2.getName() + "的郵件內容是:" + person2.getEmail().getContent()); System.out.println(person3.getName() + "的郵件內容是:" + person3.getEmail().getContent()); } } -------------------- Output: 張三的郵件內容是:請與今天12:30到二會議室參加會議... 李四的郵件內容是:請與今天12:30到二會議室參加會議... 王五的郵件內容是:請與今天12:30到二會議室參加會議...
在該應用程式中,首先定義一封郵件,然後將該郵件發給張三、李四、王五三個人,由於他們是使用相同的郵件,並且僅有名字不同,所以使用張三該物件類拷貝李四、王五物件然後更改下名字即可。程式一直到這裡都沒有錯,但是如果我們需要張三提前30分鐘到,即把郵件的內容修改下:
public class Client { public static void main(String[] args) { //寫封郵件 Email email = new Email("請參加會議","請與今天12:30到二會議室參加會議..."); Person person1 = new Person("張三",email); Person person2 = person1.clone(); person2.setName("李四"); Person person3 = person1.clone(); person3.setName("王五"); person1.getEmail().setContent("請與今天12:00到二會議室參加會議..."); System.out.println(person1.getName() + "的郵件內容是:" + person1.getEmail().getContent()); System.out.println(person2.getName() + "的郵件內容是:" + person2.getEmail().getContent()); System.out.println(person3.getName() + "的郵件內容是:" + person3.getEmail().getContent()); } }
在這裡同樣是使用張三該物件實現對李四、王五拷貝,最後將張三的郵件內容改變為:請與今天12:00到二會議室參加會議...。但是結果是:
張三的郵件內容是:請與今天12:00到二會議室參加會議...
李四的郵件內容是:請與今天12:00到二會議室參加會議...
王五的郵件內容是:請與今天12:00到二會議室參加會議...
這裡我們就有疑惑為什麼李四和王五的郵件內容也發生改變了呢?其實出現問題的關鍵就在於clone()方法上面,我們知道clone()方法是使用Object類的clone()方法,但是該方法存在一個缺陷,他並不會將物件的所有屬性全部拷貝過來,而是有選擇性的拷貝,基本規則如下:
(1)基本型別:
如果變數是基本型別,則拷貝其值,比如Int、float等。
(2)物件:
如果變數是一個例項物件,則拷貝其地址引用,也就是說此時新物件與原來物件是公用該例項變數。
(3)String字串:
如果變數為String字串,則拷貝其引用地址,但是在修改的時候,它會從字串池中重新生成一個新的字串,原有的字串物件保持不變。
基於上面上面的規則,我們很容易發現問題的所在,他們三者公用一個物件,張三修改了該郵件內容,則李四和王五也會修改,所以才會出現上面的情況。對於這種情況我們還是可以解決的,只需要在clone()方法裡面新建一個物件,然後張三引用該物件即可(深拷貝):
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提供的一種簡單的拷貝機制,不便於直接使用。
對於上面的解決方案還是存在一個問題,若我們系統中存在大量的物件是通過拷貝生成的,如果我們每一個類都寫一個clone()方法,並將還需要進行深拷貝,新建大量的物件,這個工程是非常大的,這裡我們可以利用序列化來實現物件的拷貝。
二、利用序列化實現物件的拷貝:
利用序列化來做深複製,把物件寫到流裡的過程是序列化(Serilization)過程,而把物件從流中讀出來的過程則叫做反序列化(Deserialization)過程。應當指出的是,寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面。利用這個特性,可以做深拷貝 。並且該新物件與母物件之間並不存在引用共享的問題,真正實現物件的深拷貝。
public class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj){
T cloneObj = null;
try {
//寫入位元組流,將該物件序列化成流,因為寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面。所以利用這個特性可以實現物件的深拷貝
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;
}
}
使用該工具類的物件必須要實現Serializable介面,否則是沒有辦法實現克隆的。
public class Person implements Serializable{
private static final long serialVersionUID = 2631590509760908280L;
..................
//去除clone()方法
}
public class Email implements Serializable{
private static final long serialVersionUID = 1267293988171991494L;
....................
}
所以使用該工具類的物件只要實現Serializable介面就可實現物件的克隆,無須繼承Cloneable介面實現clone()方法。
public class Client {
public static void main(String[] args) {
//寫封郵件
Email email = new Email("請參加會議","請與今天12:30到二會議室參加會議...");
Person person1 = new Person("張三",email);
Person person2 = CloneUtils.clone(person1);
person2.setName("李四");
Person person3 = CloneUtils.clone(person1);
person3.setName("王五");
person1.getEmail().setContent("請與今天12:00到二會議室參加會議...");
System.out.println(person1.getName() + "的郵件內容是:" + person1.getEmail().getContent());
System.out.println(person2.getName() + "的郵件內容是:" + person2.getEmail().getContent());
System.out.println(person3.getName() + "的郵件內容是:" + person3.getEmail().getContent());
}
}
-------------------
Output:
張三的郵件內容是:請與今天12:00到二會議室參加會議...
李四的郵件內容是:請與今天12:30到二會議室參加會議...
王五的郵件內容是:請與今天12:30到二會議室參加會議...