什麼是深度克隆?
淺克隆僅複製引用,深度克隆遞歸複製對象圖中所有可達對象,確保原對象與副本完全隔離。序列化法利用 JVM 序列化機制「寫出→讀入」實現深度複製,是通用性最強的方案之一。
序列化法核心實現
public final class SerializationCloneUtil {
private SerializationCloneUtil() {}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T obj) {
if (obj == null) return null;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
oos.flush();
try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new IllegalStateException("深度克隆失敗: " + obj.getClass(), e);
}
}
}
完整可運行範例
public class DeepCloneDemo implements Serializable {
private static class Address implements Serializable {
String city; String street;
Address(String city, String street) { this.city = city; this.street = street; }
@Override public String toString() { return city + " " + street; }
}
private static class Person implements Serializable {
String name; int age; Address address; List<String> tags;
Person(String name, int age, Address addr, List<String> tags) {
this.name = name; this.age = age; this.address = addr; this.tags = tags;
}
@Override public String toString() {
return "Person{name='" + name + "', age=" + age + ", addr=" + address + ", tags=" + tags + "}";
}
}
public static void main(String[] args) {
Person original = new Person("Henry", 22,
new Address("Taipei", "Renai Rd"),
Arrays.asList("java", "spring"));
Person cloned = SerializationCloneUtil.deepClone(original);
// 驗證隔離性
cloned.getTags().add("cloned-tag");
cloned.getAddress().city = "Kaohsiung";
System.out.println("Original: " + original);
System.out.println("Cloned: " + cloned);
// 原對象 tags 不變,address.city 仍為 Taipei
}
}
序列化法優缺點分析
| 優點 | 缺點 |
| 通用性強:任意實現 Serializable 的對象圖皆可複製 | 效能較差:涉及字節流編解碼、反射,比手寫複製慢 10-100 倍 |
| 零侵入:無需修改業務類、無需實現 Cloneable | 要求所有可達類實現 Serializable,否則拋 NotSerializableException |
| 自動處理循環引用、複雜集合、代理對象 | transient 欄位會丟失(需自行處理) |
| 支援跨 JVM 進程傳輸(天然支援分散式) | 無法複製非序列化資源(Socket、Thread、DB Connection) |
主流替代方案對比
| 方案 | 效能 | 適用場景 | 備註 |
| 手寫複製構造器/Setter | 最高 | 核心領域模型、高頻調用 | 維護成本高,易漏欄位 |
Apache Commons Lang SerializationUtils.clone() | 同序列化法 | 快速接入、舊專案 | 內部同樣用序列化 |
| JSON 循環轉換 | 中等 | DTO、VO、跨語言互操作 | 需無參構造器、失去型別資訊(多態) |
MapStruct / Lombok @Builder | 編譯期生成,極快 | 新專案、DTO 轉換 | 需註解處理器,不支援任意對象圖 |
| Kryo / FST / Protobuf | 極高 | 高效能序列化、緩存、RPC | 需註冊類、配置較複雜 |
選型建議
- 低頻、通用、對象圖複雜:序列化法(或 Commons Lang 封裝)
- 高頻、核心領域模型:手寫複製或 MapStruct
- DTO/VO 層轉換:MapStruct / Lombok Builder / JSON
- 分散式緩存/消息傳輸:Kryo / Protobuf / JSON
避坑提醒
- 確保所有可達類實現
Serializable,建議加上 serialVersionUID。
- 標記不需複製的欄位為
transient(如 Logger、緩存)。
- 大對象圖克隆可能導致 OOM,考慮分批處理或改用流式複製。
- 單元測試驗證:修改副本不影響原對象、循環引用不棧溢出。