對象克隆
對象克隆最簡單的方式是:將對原對象的引用直接傳給一個新的副本變量。這種方式存在很大的缺陷,兩個變量中任何一個變量的改變都會影響另一個變量。
淺拷貝
利用Object
類的clone
方法,能夠創建一個新的對象,並拷貝原對象的域 ,返回新對象的引用。
優點:使副本的操作與原變量的操作相對獨立。
缺點:當淺克隆對象與原對象共享的子對象可變時,對可變子對象的操作仍會同時影響兩個對象。
// 原對象,包含Date類子對象
Employee original = new Employee("John Public", 50000);
// 使用clone方法,兩個對象共享對Date類子對象的引用
Employee copy = original.clone();
// 若修改Date類子對象,則兩個對象中的Date類子對象的值都會改變
在上述情況中,由於Date
類是可變的,導致淺克隆對象和原對象之間仍然存在影響。作為替代,應當使用不可變的LocalDate
類。
Cloneable接口
實際上,Object
類中的clone
方法被聲明為protected
。因此,Object
的子類只能調用clone
來克隆它們自己的對象,而不能克隆其他類的對象。
注意:Cloneable
接口是Java
提供的標記接口。標記接口不包含任何方法,其唯一的作用是允許在類型查詢中使用instanceof
。
即使clone
的默認實現(淺拷貝)能夠滿足要求,仍然需要實現Cloneable
接口,並將clone
重新定義為public
,再調用super.clone()
。
class Employee implements Cloneable {
// 如果調用clone的類沒有實現Cloneable接口,則拋出CloneNotSupportedException異常
public Employee clone() throws CloneNotSupportedException {
// 調用超類的clone方法
return (Employee) super.clone();
}
}
深拷貝
當淺拷貝無法滿足對象之間的獨立性時,需要建立深拷貝,即克隆對象中可變的實例域。
class Employee implements Cloneable {
// 如果調用clone的類沒有實現Cloneable接口,則拋出CloneNotSupportedException異常
public Employee clone() throws CloneNotSupportedException {
// 調用超類的clone方法
Employee cloned = super.clone();
// 對可變的hireDay子對象再次建立淺拷貝
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
序列化
對象序列化是Java
語言支持的一種非常通用的機制,它可以將任何對象寫出到輸出流中,並在之后將其讀回。使用對象序列化機制可能便捷地處理多態集合(例如,一個Employee
記錄數組包含Manager
等子類實例)。
保存和加載序列化對象
使用ObjectOutputStream
對象的writeObject
方法保存數據對象,使用ObjectInputStream
對象的readObject
讀回數據對象。使用序列化時必須實現Serializable
接口,其和Cloneable
接口很相似,沒有任何方法,其他類不需要為實現它做出任何改動。
// 創建一個Employee對象和一個Manager對象
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
// 將兩個對象存儲在對象輸出流中
ObjectOutputStream out = new PbjectOutputStream(new FileOutputStream("employee.dat"));
out.writeObject(harry);
out.writeObject(boss);
// 將兩個對象從對象輸入流中恢復
ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"));
Employee e1 = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();
對象序列化工作機制如下:
- 每一個對象引用關聯一個序列號。
- 第一次遇到每個對象時,保存其對象數據到輸出流中。
- 如果某個對象之前已經被保存過,那么只寫出“與之前保存過的序列號為x的對象相同”。
- 讀回時,過程相反:對於對象輸入流中的對象,在第一次遇到其序列號時,構建它,並使用流中數據來初始化它,然后記錄這個序列號和新對象之間的關聯。
- 當遇到“與之前保存過的序列號x的對象相同”標記時,獲取與這個序列號相關聯的對象引用。
注:序列化使用序列號代替了內存地址,允許將對象集合在機器之間進行傳送。
對象序列化的文件格式
注:以下(x)格式均表示字節長度。
對象序列化文件開頭:魔幻數字(2) + 版本號(2)
類描述符的存儲格式:
72 + 類名長度(2) + 類名
指紋(8) + 標志(1) + 數據域描述符的數量(2) + 數據域描述符 + 78 + 超類類型(無超類為70)
其中,指紋指通過對類、超類、接口、域類型和方法簽名按照規范方式排序,然后應用安全散列算法SHA得到的。SHA將數據塊轉換為20個字節的數據包,序列化機制只使用SHA的前8個字節。
標志字節由java.io.ObjectStreamConstants
中定義的3位掩碼構成:
static final byte SC_WRITE_METHOD = 1;
// class has a writeObject_method that writes additional data
static final byte SC_SERIALIZABLE = 2;
// class implements the Serializable interface
static final byte SC_EXTERNALIZABLE = 4;
// class implements the Externalizable interface
數據域描述符的格式如下:
類型編碼(1) + 域名長度(2) + 域名 + 類名(若域是對象)
對象序列化的文件格式特點如下:
- 對象流輸出中包含所有對象的類型和數據域。
- 每個對象都被賦予一個序列號。
- 相同對象的重復出現將被存儲為對這個對象的序列號的引用。
修改默認的序列化機制
對於不可序列化的數據域,如只對本地方法有意義的存儲文件句柄值等,序列化機制將其標記為transient
。不可序列化的類的域也需要標記為transient
。瞬時的域在對象序列化時被跳過。
在可序列化的類中可以定義writeObject
方法和readObject
方法:
private void writeObject(ObjectOutputStream out)
throws IOException;
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException;
定義上述方法的類在序列化時會調用這兩個方法對其數據域進行序列化。
由於writeObject
和readObject
方法不關心超類數據和任何其他類的信息,若要對整個對象的存儲和恢復負責,就要實現Externalizable
接口,定義writeExternal
方法和readExternal
方法:
public void writeExternal(ObjectOutputStream out)
throws IOException;
public void readExternal(ObjectInputStream in)
throws IOException, ClassNoutFoundException;
注意:writeObject
和readObject
方法是私有的,且只能被序列化機制調用。而writeExternal
和readExternal
方法是共有的,且readExternal
方法甚至潛在地允許修改現有對象的狀態。
使用序列化技術克隆對象
只需將對象序列化到輸出流中,然后將其讀回,就能產生現有對象的深拷貝。在此過程中,可以使用ByteArrayOutputStream
方法將數據保存到字節數組中。如下修改clone
方法:
class SerialCloneable implements Cloneable, Serializable {
public Object clone() throws CloneNotSupportedException {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (ObjectOutputStream = new ObjectOutputStream(bout)) {
out.writeObject(this);
}
try (InputSteam bin = new ByteArrayInputStream(bout.toByteArray())) {
ObjectInputStream in = new ObjectInputStream(bin);
return in.readObject();
}
catch (IOException | ClassNotFoundException e) {
CloneNotSupportedException e2 = new CloneNotSupportException();
e2.initCause(e);
throw e2;
}
}
}
}
需要克隆的類只需繼承SerialCloneable
類即可。