關(guān)于Java中Clonable接口和深拷貝詳解
前言
在 Java 中,Cloneable 接口是實(shí)現(xiàn)對(duì)象拷貝的核心機(jī)制之一,但它默認(rèn)僅支持 淺拷貝;而 深拷貝 是基于淺拷貝的進(jìn)階需求,用于解決引用類型成員共享的問題。
一、先搞懂:Cloneable接口是什么?
1. 核心定位:標(biāo)記接口(Marker Interface)
Cloneable 是 Java 中的 標(biāo)記接口(無任何抽象方法),僅用于告訴 JVM:“這個(gè)類的對(duì)象允許被克?。?/span>clone())”。
- 定義(源碼極簡):
public interface Cloneable {} // 無任何方法! - 關(guān)鍵依賴:克隆的核心實(shí)現(xiàn)并非來自
Cloneable接口,而是來自java.lang.Object類的clone()方法:// Object 類的 clone() 方法(protected 修飾,需重寫才能公開調(diào)用) protected native Object clone() throws CloneNotSupportedException;
2. 淺拷貝(Shallow Copy):Cloneable的默認(rèn)行為
(1)淺拷貝的定義
當(dāng)對(duì)象被淺拷貝時(shí),會(huì)創(chuàng)建一個(gè) 新的對(duì)象實(shí)例,但對(duì)象中的 引用類型成員變量 不會(huì)被復(fù)制(新對(duì)象和原對(duì)象共享同一個(gè)引用類型成員)。
- 簡單說:“拷貝對(duì)象本身,不拷貝對(duì)象里的‘引用子對(duì)象’”。
(2)實(shí)現(xiàn)淺拷貝的3 個(gè)步驟
- 類實(shí)現(xiàn)
Cloneable接口(標(biāo)記允許克?。?/span> - 重寫
Object類的clone()方法(提升訪問權(quán)限為public,處理異常); - 調(diào)用
super.clone()完成淺拷貝(JVM 原生實(shí)現(xiàn)對(duì)象拷貝)。
(3)代碼示例:淺拷貝實(shí)戰(zhàn)
// 引用類型:地址(被 User 類引用)
class Address {
private String city;
public Address(String city) {
this.city = city;
}
// getter/setter
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
}
// 實(shí)現(xiàn) Cloneable 接口,支持淺拷貝
class User implements Cloneable {
private String name; // 基本類型包裝類(不可變,淺拷貝無問題)
private int age; // 基本類型(淺拷貝直接復(fù)制值)
private Address addr;// 引用類型(淺拷貝僅復(fù)制引用,共享對(duì)象)
// 構(gòu)造方法
public User(String name, int age, Address addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
// 重寫 clone() 方法:實(shí)現(xiàn)淺拷貝
@Override
public User clone() throws CloneNotSupportedException {
// 調(diào)用 Object 的 clone(),JVM 會(huì)創(chuàng)建新對(duì)象并復(fù)制成員值
return (User) super.clone();
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Address getAddr() { return addr; }
}
// 測試淺拷貝
public class ShallowCopyTest {
public static void main(String[] args) throws CloneNotSupportedException {
// 1. 創(chuàng)建原對(duì)象
Address addr = new Address("北京");
User user1 = new User("張三", 20, addr);
// 2. 淺拷貝得到新對(duì)象
User user2 = user1.clone();
// 3. 驗(yàn)證:基本類型/不可變類型的拷貝(獨(dú)立)
System.out.println(user1.getName() == user2.getName()); // true(String 不可變,共享無問題)
user2.setName("李四");
user2.setAge(22);
System.out.println(user1.getName()); // 張三(原對(duì)象不受影響)
System.out.println(user1.getAge()); // 20(原對(duì)象不受影響)
// 4. 驗(yàn)證:引用類型的拷貝(共享)
System.out.println(user1.getAddr() == user2.getAddr()); // true(同一個(gè) Address 對(duì)象)
user2.getAddr().setCity("上海"); // 修改 user2 的 Address
System.out.println(user1.getAddr().getCity()); // 上海(原對(duì)象的 Address 也被修改?。?
}
}(4)淺拷貝的問題
當(dāng)對(duì)象包含 可變的引用類型成員(如 Address)時(shí),淺拷貝會(huì)導(dǎo)致原對(duì)象和拷貝對(duì)象共享該成員,修改其中一個(gè)會(huì)影響另一個(gè),破壞對(duì)象的 “獨(dú)立性”—— 這就是需要深拷貝的場景。
二、深拷貝(Deep Copy):解決引用類型共享問題
1. 深拷貝的定義
深拷貝會(huì)創(chuàng)建一個(gè) 完全獨(dú)立的新對(duì)象:不僅拷貝對(duì)象本身,還會(huì)遞歸拷貝對(duì)象中所有 可變的引用類型成員變量,最終新對(duì)象和原對(duì)象的引用類型成員指向不同的內(nèi)存地址,互不影響。
- 簡單說:“拷貝對(duì)象本身 + 拷貝對(duì)象里所有的‘引用子對(duì)象”。
2. 深拷貝的 3 種實(shí)現(xiàn)方式(實(shí)戰(zhàn)常用)
方式 1:重寫clone()方法,手動(dòng)遞歸拷貝引用類型
核心思路:在淺拷貝的基礎(chǔ)上,對(duì)每個(gè)引用類型成員也調(diào)用其 clone() 方法(需讓引用類型也實(shí)現(xiàn) Cloneable)。
// 步驟1:讓引用類型 Address 也實(shí)現(xiàn) Cloneable,支持拷貝
class Address implements Cloneable {
private String city;
public Address(String city) { this.city = city; }
// 重寫 clone() 方法
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
// getter/setter 略
}
// 步驟2:User 類的 clone() 中,手動(dòng)拷貝 Address
class User implements Cloneable {
private String name;
private int age;
private Address addr;
// 構(gòu)造方法略
@Override
public User clone() throws CloneNotSupportedException {
// 第一步:淺拷貝 User 對(duì)象本身
User newUser = (User) super.clone();
// 第二步:手動(dòng)拷貝引用類型成員(深拷貝核心)
newUser.addr = this.addr.clone(); // 遞歸拷貝 Address
return newUser;
}
// getter/setter 略
}
// 測試深拷貝
public class DeepCopyTest1 {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("北京");
User user1 = new User("張三", 20, addr);
User user2 = user1.clone();
// 驗(yàn)證:引用類型成員不再共享
System.out.println(user1.getAddr() == user2.getAddr()); // false(不同 Address 對(duì)象)
user2.getAddr().setCity("上海");
System.out.println(user1.getAddr().getCity()); // 北京(原對(duì)象不受影響)
System.out.println(user2.getAddr().getCity()); // 上海(新對(duì)象獨(dú)立修改)
}
}方式 2:通過序列化(Serializable)實(shí)現(xiàn)深拷貝
核心思路:利用 Java 序列化將對(duì)象寫入流(序列化),再從流中讀?。ǚ葱蛄谢?/span>,生成的新對(duì)象是完全獨(dú)立的(無需手動(dòng)遞歸拷貝)。
- 優(yōu)點(diǎn):無需讓每個(gè)引用類型都實(shí)現(xiàn)
Cloneable,適合復(fù)雜對(duì)象(多層引用嵌套); - 缺點(diǎn):需要所有成員變量都實(shí)現(xiàn)
Serializable接口,效率略低于手動(dòng)克隆。
import java.io.*;
// 步驟1:所有相關(guān)類實(shí)現(xiàn) Serializable 接口(標(biāo)記可序列化)
class Address implements Serializable {
private String city;
public Address(String city) { this.city = city; }
// getter/setter 略
}
class User implements Serializable {
private String name;
private int age;
private Address addr;
public User(String name, int age, Address addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
// 步驟2:實(shí)現(xiàn)深拷貝工具方法(序列化+反序列化)
public User deepCopy() throws IOException, ClassNotFoundException {
// 1. 序列化:將對(duì)象寫入字節(jié)流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this); // 寫入當(dāng)前 User 對(duì)象
// 2. 反序列化:從字節(jié)流讀取新對(duì)象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (User) ois.readObject(); // 生成獨(dú)立的新對(duì)象
}
// getter/setter 略
}
// 測試序列化深拷貝
public class DeepCopyTest2 {
public static void main(String[] args) throws Exception {
Address addr = new Address("北京");
User user1 = new User("張三", 20, addr);
User user2 = user1.deepCopy();
// 驗(yàn)證:完全獨(dú)立
System.out.println(user1.getAddr() == user2.getAddr()); // false
user2.getAddr().setCity("上海");
System.out.println(user1.getAddr().getCity()); // 北京
System.out.println(user2.getAddr().getCity()); // 上海
}
}方式 3:使用第三方工具(簡化開發(fā))
實(shí)際開發(fā)中,可借助成熟工具類避免重復(fù)編碼,常用的有:
- Apache Commons Lang:
SerializationUtils.clone()(基于序列化,無需手動(dòng)寫流操作); - Gson/Jackson:將對(duì)象轉(zhuǎn)為 JSON 字符串,再轉(zhuǎn)回對(duì)象(間接實(shí)現(xiàn)深拷貝,支持復(fù)雜對(duì)象)。
示例(Apache Commons Lang):
- 引入依賴(Maven):
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> - 代碼實(shí)現(xiàn):
import org.apache.commons.lang3.SerializationUtils; // 所有類仍需實(shí)現(xiàn) Serializable class User implements Serializable { /* 成員變量、構(gòu)造方法略 */ } public class DeepCopyTest3 { public static void main(String[] args) { Address addr = new Address("北京"); User user1 = new User("張三", 20, addr); // 一行代碼實(shí)現(xiàn)深拷貝 User user2 = SerializationUtils.clone(user1); System.out.println(user1.getAddr() == user2.getAddr()); // false } }
3. 深拷貝的注意事項(xiàng)
- 循環(huán)引用處理:如果對(duì)象存在循環(huán)引用(如
A引用B,B引用A),手動(dòng)克隆會(huì)棧溢出,序列化方式(或工具類)可自動(dòng)處理; - 不可變類型無需深拷貝:如
String、Integer等不可變類型,淺拷貝時(shí)共享引用無問題(無法修改原值); - ** transient 關(guān)鍵字 **:序列化方式中,被
transient修飾的成員變量不會(huì)被拷貝(需根據(jù)需求決定是否使用)。
三、淺拷貝 vs 深拷貝 核心區(qū)別

四、常見面試題 & 實(shí)戰(zhàn)建議
1. 面試高頻問題
(1)Cloneable接口有什么用?如果不實(shí)現(xiàn)它,調(diào)用clone()會(huì)怎樣?
- 作用:標(biāo)記類的對(duì)象允許被克隆(無任何方法,僅為 JVM 提供標(biāo)記);
- 不實(shí)現(xiàn)后果:調(diào)用
super.clone()時(shí)會(huì)拋出CloneNotSupportedException(運(yùn)行時(shí)異常)。
(2)Object類的clone()方法是淺拷貝還是深拷貝?
默認(rèn)是 淺拷貝:僅復(fù)制對(duì)象的成員值(基本類型復(fù)制值,引用類型復(fù)制引用地址)。
(3)深拷貝的實(shí)現(xiàn)方式有哪些?各自的優(yōu)缺點(diǎn)?
- 手動(dòng)克隆:優(yōu)點(diǎn)效率高,缺點(diǎn)需遞歸實(shí)現(xiàn),復(fù)雜對(duì)象繁瑣;
- 序列化:優(yōu)點(diǎn)無需手動(dòng)處理嵌套,缺點(diǎn)需實(shí)現(xiàn)
Serializable,效率略低; - 第三方工具:優(yōu)點(diǎn)簡潔高效,缺點(diǎn)依賴外部依賴。
(4)為什么String類型在淺拷貝中不會(huì)有問題?
String 是 不可變類型(一旦創(chuàng)建無法修改,修改時(shí)會(huì)生成新 String 對(duì)象),淺拷貝時(shí)共享引用不會(huì)導(dǎo)致原對(duì)象被修改,因此無需深拷貝。
2. 實(shí)戰(zhàn)選擇建議
- 簡單對(duì)象(無可變引用類型):用
Cloneable實(shí)現(xiàn)淺拷貝(高效簡潔); - 復(fù)雜對(duì)象(多層嵌套引用):優(yōu)先用第三方工具(如 Apache Commons Lang)或序列化(減少編碼量);
- 性能敏感場景:手動(dòng)實(shí)現(xiàn)深拷貝(避免序列化的性能損耗);
- 避免過度使用克隆:如果對(duì)象可通過構(gòu)造方法創(chuàng)建新實(shí)例(如
new User(user1.getName(), user1.getAge(), new Address(...))),可直接用構(gòu)造方法替代克?。ǜ鬃x、無接口依賴)。
總結(jié)
- Cloneable 是 “允許克隆” 的標(biāo)記接口,默認(rèn)支持淺拷貝,核心依賴 Object.clone();
- 淺拷貝適合簡單對(duì)象,深拷貝解決引用類型共享問題,需通過手動(dòng)遞歸、序列化或工具類實(shí)現(xiàn);
- 實(shí)際開發(fā)中,優(yōu)先選擇第三方工具(如 SerializationUtils)實(shí)現(xiàn)深拷貝,兼顧簡潔性和穩(wěn)定性。
到此這篇關(guān)于關(guān)于Java中Clonable接口和深拷貝的文章就介紹到這了,更多相關(guān)Java中Clonable接口和深拷貝內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java連接數(shù)據(jù)庫實(shí)現(xiàn)方式
文章講述了Java連接MySQL數(shù)據(jù)庫的詳細(xì)步驟,包括下載和導(dǎo)入JDBC驅(qū)動(dòng)、創(chuàng)建數(shù)據(jù)庫和表、以及編寫連接和讀取數(shù)據(jù)的代碼2024-11-11
SpringCloud中的Feign遠(yuǎn)程調(diào)用接口傳參失敗問題
這篇文章主要介紹了SpringCloud中的Feign遠(yuǎn)程調(diào)用接口傳參失敗問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
java定時(shí)任務(wù)實(shí)現(xiàn)的4種方式小結(jié)
這篇文章主要介紹了java定時(shí)任務(wù)實(shí)現(xiàn)的4種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法
本文主要介紹了SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法,主要介紹了兩種方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
關(guān)于weblogic部署Java項(xiàng)目的包沖突問題的解決
這篇文章主要介紹了關(guān)于weblogic部署Java項(xiàng)目的包沖突問題的解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01
java String[]字符串?dāng)?shù)組自動(dòng)排序的簡單實(shí)現(xiàn)
下面小編就為大家?guī)硪黄猨ava String[]字符串?dāng)?shù)組自動(dòng)排序的簡單實(shí)現(xiàn)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-09-09

