什麼是 Java 對象深拷貝?面試必問!
- 2019 年 10 月 4 日
- 筆記
介紹
在Java語言里,當我們需要拷貝一個對象時,有兩種類型的拷貝:淺拷貝與深拷貝。
淺拷貝只是拷貝了源對象的地址,所以源對象的值發生變化時,拷貝對象的值也會發生變化。
而深拷貝則是拷貝了源對象的所有值,所以即使源對象的值發生變化時,拷貝對象的值也不會改變。如下圖描述:

了解了淺拷貝和深拷貝的區別之後,本篇部落格將教大家幾種深拷貝的方法。
拷貝對象
首先,我們定義一下需要拷貝的簡單對象。
/** * 用戶 */ public class User { private String name; private Address address; // constructors, getters and setters } /** * 地址 */ public class Address { private String city; private String country; // constructors, getters and setters }
如上述程式碼,我們定義了一個User用戶類,包含name姓名,和address地址,其中address並不是字元串,而是另一個Address類,包含country國家和city城市。構造方法和成員變數的get()、set()方法此處我們省略不寫。接下來我們將詳細描述如何深拷貝User對象。
方法一 構造函數
我們可以通過在調用構造函數進行深拷貝,形參如果是基本類型和字元串則直接賦值,如果是對象則重新new一個。
測試用例
@Test public void constructorCopy() { Address address = new Address("杭州", "中國"); User user = new User("大山", address); // 調用構造函數時進行深拷貝 User copyUser = new User(user.getName(), new Address(address.getCity(), address.getCountry())); // 修改源對象的值 user.getAddress().setCity("深圳"); // 檢查兩個對象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法二 重載clone()方法
Object父類有個clone()的拷貝方法,不過它是protected類型的,我們需要重寫它並修改為public類型。除此之外,子類還需要實現Cloneable介面來告訴JVM這個類是可以拷貝的。
重寫程式碼
讓我們修改一下User類,Address類,實現Cloneable介面,使其支援深拷貝。
/** * 地址 */ public class Address implements Cloneable { private String city; private String country; // constructors, getters and setters @Override public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } }
/** * 用戶 */ public class User implements Cloneable { private String name; private Address address; // constructors, getters and setters @Override public User clone() throws CloneNotSupportedException { User user = (User) super.clone(); user.setAddress(this.address.clone()); return user; } }
需要注意的是,super.clone()其實是淺拷貝,所以在重寫User類的clone()方法時,address對象需要調用address.clone()重新賦值。
測試用例
@Test public void cloneCopy() throws CloneNotSupportedException { Address address = new Address("杭州", "中國"); User user = new User("大山", address); // 調用clone()方法進行深拷貝 User copyUser = user.clone(); // 修改源對象的值 user.getAddress().setCity("深圳"); // 檢查兩個對象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法三 Apache Commons Lang序列化
Java提供了序列化的能力,我們可以先將源對象進行序列化,再反序列化生成拷貝對象。但是,使用序列化的前提是拷貝的類(包括其成員變數)需要實現Serializable介面。Apache Commons Lang包對Java序列化進行了封裝,我們可以直接使用它。
重寫程式碼
讓我們修改一下User類,Address類,實現Serializable介面,使其支援序列化。
/** * 地址 */ public class Address implements Serializable { private String city; private String country; // constructors, getters and setters }
/** * 用戶 */ public class User implements Serializable { private String name; private Address address; // constructors, getters and setters }
測試用例
@Test public void serializableCopy() { Address address = new Address("杭州", "中國"); User user = new User("大山", address); // 使用Apache Commons Lang序列化進行深拷貝 User copyUser = (User) SerializationUtils.clone(user); // 修改源對象的值 user.getAddress().setCity("深圳"); // 檢查兩個對象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法四 Gson序列化
Gson可以將對象序列化成JSON,也可以將JSON反序列化成對象,所以我們可以用它進行深拷貝。
測試用例
@Test public void gsonCopy() { Address address = new Address("杭州", "中國"); User user = new User("大山", address); // 使用Gson序列化進行深拷貝 Gson gson = new Gson(); User copyUser = gson.fromJson(gson.toJson(user), User.class); // 修改源對象的值 user.getAddress().setCity("深圳"); // 檢查兩個對象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法五 Jackson序列化
Jackson與Gson相似,可以將對象序列化成JSON,明顯不同的地方是拷貝的類(包括其成員變數)需要有默認的無參構造函數。
重寫程式碼
讓我們修改一下User類,Address類,實現默認的無參構造函數,使其支援Jackson。
/** * 用戶 */ public class User { private String name; private Address address; // constructors, getters and setters public User() { } }
/** * 地址 */ public class Address { private String city; private String country; // constructors, getters and setters public Address() { } }
測試用例
@Test public void jacksonCopy() throws IOException { Address address = new Address("杭州", "中國"); User user = new User("大山", address); // 使用Jackson序列化進行深拷貝 ObjectMapper objectMapper = new ObjectMapper(); User copyUser = objectMapper.readValue(objectMapper.writeValueAsString(user), User.class); // 修改源對象的值 user.getAddress().setCity("深圳"); // 檢查兩個對象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
總結
說了這麼多深拷貝的實現方法,哪一種方法才是最好的呢?
最簡單的判斷就是根據拷貝的類(包括其成員變數)是否提供了深拷貝的構造函數、是否實現了Cloneable介面、是否實現了Serializable介面、是否實現了默認的無參構造函數來進行選擇。如果需要詳細的考慮,則可以參考下面的表格:

作者丨吳大山
wudashan.com/2018/10/14/Java-Deep-Copy
– END –