詳解 Java 中的物件克隆

亦楓發表於2019-02-28

前言

在 Java 語言中,我們說兩個物件是否相等通常有兩層含義:

  • 物件的內容是否相等,通常使用到物件的 equals(Object o) 函式;

  • 引用的地址是否相同,使用運算子 == 比較即可。

當兩個物件通過賦值符號 = 賦值時,表明這兩個物件指向了記憶體中同一個地址,所以改變其中一個物件的內容,也就間接地改變了另一個物件的內容。有時候,我們需要從一個已經存在的物件重新拷貝一份出來,並且不僅這兩個物件內容相等,在記憶體中存在兩個獨立的儲存地址,互不影響,這時,就需要用到 Java 中的克隆機制。

Cloneable

通過 Cloneable 介面可以很輕鬆地實現 Java 物件的克隆,只需要 implements Cloneable 並實現 Object 的 clone() 方法即可,如:


public class User implements Cloneable{

    private String username;

    private String password;

    public User(String username, String password) {
        super();
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object obj) {
        User user = (User) obj;
        if (username.equals(user.username) && password.equals(user.password)) {
            return true;
        }
        return false;
    }

}複製程式碼

注意這裡物件實現的是 Object 類的 clone() 方法,因為 Cloneable 是一個空介面:

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}複製程式碼

從原始碼註釋中可以看出,需要實現 Object 類中的 clone() 方法(注意:clone() 函式是一個 native 方法,同時丟擲了一個異常):

    /**
     * Creates and returns a copy of this object.  The precise meaning
     * of "copy" may depend on the class of the object. The general
     * intent is that, for any object {@code x}, the expression:
     * <blockquote>
     * <pre>
     * x.clone() != x</pre></blockquote>
     * will be true, and that the expression:
     * <blockquote>
     * <pre>
     * x.clone().getClass() == x.getClass()</pre></blockquote>
     * will be {@code true}, but these are not absolute requirements.
     * While it is typically the case that:
     * <blockquote>
     * <pre>
     * x.clone().equals(x)</pre></blockquote>
     * will be {@code true}, this is not an absolute requirement.
     * <p>
     * By convention, the returned object should be obtained by calling
     * {@code super.clone}.  If a class and all of its superclasses (except
     * {@code Object}) obey this convention, it will be the case that
     * {@code x.clone().getClass() == x.getClass()}.
     * <p>
     * By convention, the object returned by this method should be independent
     * of this object (which is being cloned).  To achieve this independence,
     * it may be necessary to modify one or more fields of the object returned
     * by {@code super.clone} before returning it.  Typically, this means
     * copying any mutable objects that comprise the internal "deep structure"
     * of the object being cloned and replacing the references to these
     * objects with references to the copies.  If a class contains only
     * primitive fields or references to immutable objects, then it is usually
     * the case that no fields in the object returned by {@code super.clone}
     * need to be modified.
     * <p>
     * The method {@code clone} for class {@code Object} performs a
     * specific cloning operation. First, if the class of this object does
     * not implement the interface {@code Cloneable}, then a
     * {@code CloneNotSupportedException} is thrown. Note that all arrays
     * are considered to implement the interface {@code Cloneable} and that
     * the return type of the {@code clone} method of an array type {@code T[]}
     * is {@code T[]} where T is any reference or primitive type.
     * Otherwise, this method creates a new instance of the class of this
     * object and initializes all its fields with exactly the contents of
     * the corresponding fields of this object, as if by assignment; the
     * contents of the fields are not themselves cloned. Thus, this method
     * performs a "shallow copy" of this object, not a "deep copy" operation.
     * <p>
     * The class {@code Object} does not itself implement the interface
     * {@code Cloneable}, so calling the {@code clone} method on an object
     * whose class is {@code Object} will result in throwing an
     * exception at run time.
     *
     * @return     a clone of this instance.
     * @throws  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    protected native Object clone() throws CloneNotSupportedException;複製程式碼

從 clone() 函式的註釋中能夠看出物件與克隆物件之間的關係,測試程式碼如下(注意:我們在 User 物件中重寫了 equals() 函式):

    public static void main(String[] args) throws CloneNotSupportedException{
        User userOne, userTwo, userThree;
        userOne = new User("username", "password");
        userTwo = userOne;
        userThree = (User) userOne.clone();

        System.out.println(userTwo==userOne);            //true
        System.out.println(userTwo.equals(userOne));    //true

        System.out.println(userThree==userOne);            //false
        System.out.println(userThree.equals(userOne));    //true

    }複製程式碼

測試結果顯示,通過 clone() 函式,我們成功地從 userOne 物件中克隆出了一份獨立的 userThree 物件。

淺克隆與深克隆

談此之前,我們先看一個例子,定義一個名為 Company 的類,並新增一個型別為 User 的成員變數:


public class Company implements Cloneable{

    private User user;

    private String address;

    public Company(User user, String address) {
        super();
        this.user = user;
        this.address = address;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object obj) {
        Company company = (Company) obj;
        if (user.equals(company.getUser()) && address.equals(company.address)) {
            return true;
        }
        return false;
    }

}複製程式碼

測試程式碼及測試結果如下:

    public static void main(String[] args) throws CloneNotSupportedException{
        Company companyOne, companyTwo, companyThree;
        companyOne = new Company(new User("username", "password"), "上海市");
        companyTwo = companyOne;
        companyThree = (Company) companyOne.clone();

        System.out.println(companyTwo==companyOne);                //true
        System.out.println(companyTwo.equals(companyOne));        //true

        System.out.println(companyThree==companyOne);            //false
        System.out.println(companyThree.equals(companyOne));    //true

        System.out.println(companyThree.getUser()==companyOne.getUser());            //true ? 這裡為什麼不是false呢
        System.out.println(companyThree.getUser().equals(companyOne.getUser()));    //true

    }複製程式碼

問題來了,companyThree 與 companyOne 中的 User 是同一個物件!也就是說 companyThree 只是克隆了 companyOne 的基本資料型別的資料,而對於引用型別的資料沒有進行深度的克隆。也就是俗稱的淺克隆。

淺克隆:顧名思義,就是很表層的克隆,只克隆物件自身的引用地址;

深克隆:也稱“N層克隆”,克隆物件自身以及物件所包含的引用型別物件的引用地址。

這裡需要注意的是,對於基本資料型別(primitive)和使用常量池方式建立的String 型別,都會針對原值克隆,所以不存在引用地址一說。當然不包括他們對應的包裝類。

所以使用深克隆就可以解決上述 Company 物件克隆過後兩個 user 物件的引用地址相同的問題。我們修改一下 Company 類的 clone() 函式:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Company company = (Company) super.clone();
        company.user = (User) company.getUser().clone();
        return company;
    }複製程式碼

再執行測試程式碼,就能得到 companyThree.getUser()==companyOne.getUser() 為 false 的結果了。

Serializable實現

通過上述介紹,我們知道,實現一個物件的克隆,需要如下幾步:

  1. 物件所在的類實現 Cloneable 介面;

  2. 重寫 clone() 函式,如果包涵引用型別的成員變數,需要使用深克隆。

如果物件不包含引用型別成員或者數量少的話,使用 Cloneable 介面還能接受,但當物件包含多個引用型別的成員,同時這些成員又包含了引用型別的成員,那層層克隆豈不是相當繁瑣,並且維護不便?所以,這裡介紹一種更加方便的實現方式,使用 ObjectOutputStreamObjectOutputStream 來實現物件的序列化和反序列化:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public abstract class BeanUtils {
    @SuppressWarnings("unchecked")
    public static <T> T cloneTo(T src) throws RuntimeException {
        ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        T dist = null;
        try {
            out = new ObjectOutputStream(memoryBuffer);
            out.writeObject(src);
            out.flush();
            in = new ObjectInputStream(new ByteArrayInputStream(memoryBuffer.toByteArray()));
            dist = (T) in.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (out != null)
                try {
                    out.close();
                    out = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            if (in != null)
                try {
                    in.close();
                    in = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
        }
        return dist;
    }
}複製程式碼

只要要克隆的物件以及物件所包含的引用型別的成員物件所在的類實現了 java.io.Serializable 介面即可實現完美克隆。

關注作者


本文由 亦楓 創作並首發於 亦楓的個人部落格 ,同步授權微信公眾號:技術鳥(NiaoTech),歡迎關注。

詳解 Java 中的物件克隆

相關文章