1. 程式人生 > 實用技巧 >什麼是 Java 物件深拷貝?

什麼是 Java 物件深拷貝?

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

介紹

在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介面、是否實現了預設的無參建構函式來進行選擇。如果需要詳細的考慮,則可以參考下面的表格:

關注公眾號Java技術棧回覆"面試"獲取我整理的2020最全面試題及答案。

推薦去我的部落格閱讀更多:

1.Java JVM、集合、多執行緒、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、後端、架構、阿里巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!