1. 程式人生 > 其它 >【Java】Object的clone方法分析

【Java】Object的clone方法分析

學習視訊:https://study.163.com/course/introduction/1006177009.htm

學習目標


  1. 能夠理解clone方法的由來
  2. 能夠使用clone方法建立物件
  3. 能夠理解克隆物件和原物件的關係
  4. 能夠理解clone方法建立物件與new關鍵字和反射建立物件的不同
  5. 能夠理解淺表複製和深層複製的含義
  6. 能夠探尋物件的複製必須實現Cloneable介面的底層原始碼

1. 克隆方法的由來


問題一:什麼是克隆(clone)方法?
答:建立並返回此物件的一個副本--按照原物件,建立一個新物件(複製原物件的內容)。

問題二:已經存在new關鍵字反射技術都可以建立物件,為什麼還需要一個Object的clone方法呢?
答:必然是new關鍵字和 反射技術,存在一些弊端。看下面的例子體會弊端在哪。

1.1 new關鍵字和反射建立物件的弊端


我們來看一個需求:使用new關鍵字和反射建立內容一模一樣的物件,並且列印它們的雜湊值。
演示素材--Person:

package com.yynm.pojo;

/**
 * @Description: Person實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

演示Person類使用new關鍵字和反射建立內容一模一樣的物件,並且列印它們的雜湊值

/**
 * @Description: 使用new關鍵字和反射建立內容一模一樣的物件,並且列印它們的雜湊值
 * @Param: []
 * @return: void
 * @Author: yeyulemon
 * @Date: 2021/12/16 21:12
 */
@Test
public void test01() throws Exception {
    Person person1 = new Person();
    person1.setName("張三");
    person1.setAge(18);

    Person person2 = new Person();
    person2.setName("張三");
    person2.setAge(18);

    System.out.println(person1 + ":" + person1.hashCode());
    System.out.println(person2 + ":" + person2.hashCode());

    Class clazz = Class.forName("com.yynm.pojo.Person");
    Person person3 = (Person) clazz.getConstructor().newInstance();
    person3.setName("張三");
    person3.setAge(18);

    Person person4 = (Person) clazz.getConstructor().newInstance();
    person4.setName("張三");
    person4.setAge(18);

    System.out.println(person3 + ":" + person3.hashCode());
    System.out.println(person4 + ":" + person4.hashCode());
}

效果

com.yynm.pojo.Person@dc24521:230835489
com.yynm.pojo.Person@10bdf5e5:280884709
com.yynm.pojo.Person@6e1ec318:1847509784
com.yynm.pojo.Person@7e0b0338:2114650936

總結:通過new和反射可以建立內容一模一樣的物件。但是,建立物件之後,通過setter方法設定一模一樣的內容,如果需要建立更多內容一致的物件,那麼就需要呼叫非常多的setter方法。

接下來,使用Object的clone方法演示,更加簡便快捷,複製物件的操作!

1.2 使用clone方法建立物件


1.2.1 使用步驟

  1. 在需要clone方法的類上實現Cloneable介面
  2. 重寫clone方法,在自己的clone方法中呼叫父類的clone方法,將返回值型別強轉成本類型別,將當前clone方法修飾符改為public
  3. 在測試中呼叫物件的clone方法

1.2.2 程式碼演示

  1. 在需要clone方法的類上實現Cloneable介面
  2. 重寫clone方法,在自己的clone方法中呼叫父類的clone方法,將返回值型別強轉成本類型別,將當前clone方法修飾符改為public
package com.yynm.pojo;

/**
 * @Description: Person實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

  1. 在測試中呼叫物件的clone方法
/** 
* @Description: 在測試中呼叫物件的clone方法
* @Param: [] 
* @return: void 
* @Author: yeyulemon 
* @Date: 2021/12/16 21:33 
*/
@Test
public void test02() throws Exception {
    Person person1 = new Person();
    person1.setName("張三");
    person1.setAge(18);

    Person person2 = person1.clone();

    System.out.println(person1 + ":" + person1.hashCode());
    System.out.println(person2 + ":" + person2.hashCode());
}

效果

com.yynm.pojo.Person@dc24521:230835489
com.yynm.pojo.Person@10bdf5e5:280884709

總結:通過使用clone方法,我們發現大大的減少了建立重複物件程式碼。這也就是clone方法存在的意義。

2. 克隆出來的物件和原來的物件有什麼關係

通過上面的測試,我們已經知道了,克隆出來的物件內容一致,但是物件雜湊值不一樣,所以是不同物件。那麼兩個物件的內容之間有什麼關聯呢?兩個物件的內容是彼此獨立,還是,兩個物件底層使用的同一個內容呢?
素材(新Person):

package com.yynm.pojo;

/**
 * @Description: Person實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Children children;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

素材(新Children):

package com.yynm.pojo;

/**
 * @Description: Children實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:49
 **/
public class Children {
    private String name;
    private int age;

    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;
    }
}

測試程式碼:

/**
* @Description: 測試克隆物件和原物件之間成員變數的聯絡
 * @Param: []
 * @return: void
 * @Author: yeyulemon
 * @Date: 2021/12/16 21:51
 */
@Test
public void test03() throws Exception {
    Person person1 = new Person();
    person1.setName("張三");
    person1.setAge(18);
    Children children = new Children();
    children.setName("李四");
    children.setAge(18);
    person1.setChildren(children);

    Person person2 = person1.clone();

    System.out.println(person1 + ":" + person1.hashCode() + ";" + children + ":" +person1.getChildren().hashCode());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + children + ":" +person2.getChildren().hashCode());
}

效果:

com.yynm.pojo.Person@10bdf5e5:280884709;com.yynm.pojo.Children@6e1ec318:1847509784
com.yynm.pojo.Person@7e0b0338:2114650936;com.yynm.pojo.Children@6e1ec318:1847509784

結論:通過測試發現克隆出來的物件雖然不一致,但是底層的成員變數的雜湊值是一致的。
這種複製我們稱之為:淺表複製
淺表複製記憶體結構:

3. 能不能讓克隆出來的物件其中成員變數也變成新物件

3.1 淺表複製的弊端

由於淺表複製導致克隆的物件中成員變數的底層雜湊值一致,如果我們操作其中一個物件的成員變數內容,就會導致所有的克隆物件的成員變數內容發生改變。
測試程式碼:

@Test
public void test04() throws Exception {
    Person person1 = new Person();
    Children children = new Children();
    children.setName("張三");
    children.setAge(18);
    person1.setChildren(children);
    Person person2 = person1.clone();
    System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().getName());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().getName());
    children.setName("李四");
    children.setAge(18);
    System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().getName());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().getName());
}

效果:

com.yynm.pojo.Person@573fd745:1463801669;張三
com.yynm.pojo.Person@15327b79:355629945;張三
com.yynm.pojo.Person@573fd745:1463801669;李四
com.yynm.pojo.Person@15327b79:355629945;李四

結論:clone方法預設的賦值操作是淺表複製,淺表複製存在弊端--僅僅建立新的物件,物件的成員內容底層雜湊值是一致的,因此,不管是原物件還是克隆物件,只有其中一個修改了成員的資料,就會影響所有的原物件和克隆物件。
要解決淺表複製的問題:進行深層的複製。

3.2 深層複製

目的:不僅在執行克隆的時候,克隆物件是一個新物件,而且,克隆物件中的成員變數,也要求是一個新的物件。

3.2.1 開發步驟

  1. 修改Children類實現Cloneable介面
  2. 修改Children類重寫clone方法
  3. 修改Person類重寫clone方法,在clone方法中呼叫children的clone方法

3.2.2 程式碼實現

  1. 修改Children類實現Cloneable介面
  2. 修改Children類實現clone方法
    Children類:
package com.yynm.pojo;

/**
 * @Description: Children實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:49
 **/
public class Children implements Cloneable {
    private String name;
    private Integer age;

    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;
    }

    @Override
    public Children clone() throws CloneNotSupportedException {
        return (Children) super.clone();
    }
}

Person類:

package com.yynm.pojo;

/**
 * @Description: Person實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Children children;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
         Person personClone = (Person) super.clone();
         personClone.setChildren(this.children.clone());
        return personClone;
    }
}

測試程式碼:

@Test
public void test05() throws Exception {
    Person person1 = new Person();
    Children children = new Children();
    children.setName("張三");
    children.setAge(18);
    person1.setChildren(children);
    Person person2 = person1.clone();
    System.out.println(person1 + ":" + person1.hashCode() + ";" + person1.getChildren().hashCode());
    System.out.println(person2 + ":" + person2.hashCode() + ";" + person2.getChildren().hashCode());
}

效果:

com.yynm.pojo.Person@573fd745:1463801669;355629945
com.yynm.pojo.Person@4f2410ac:1327763628;1915503092

深層複製記憶體結構:

4. 使用clone介面實現深層複製的弊端

4.1 使用clone介面實現深層複製的弊端

以上的方法雖然完成了深層複製,但是修改類中成員變數對應的原始碼,如果成員變數特別多,那麼就需要修改多個類的原始碼。
例如以下程式碼,我們就需要修改兩個成員變數對應類的原始碼(Children,Grandson):

package com.yynm.pojo;

/**
 * @Description: Person實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Children children;
    private Grandson grandson;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    public Grandson getGrandson() {
        return grandson;
    }

    public void setGrandson(Grandson grandson) {
        this.grandson = grandson;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        Person personClone = (Person) super.clone();
        personClone.setChildren(this.children.clone());
        personClone.setGrandson(this.grandson.clone());
        return personClone;
    }
}

結論:使用克隆介面完成深度複製的弊端:

1. 重複實現Cloneable介面
2. 重複實現clone方法
3. 重複改寫Person類的clone方法

可以使用IO流的方式進行復制操作(深度複製),可以解決重複修改原始碼的問題。

4.2 使用IO進行克隆複製(深度複製)

4.2.1 使用IO複製相關的API介紹

1、ByteArrayOutputStream

構造方法:

2、ByteArrayInputStream

構造方法:

3、ObjectOutputStream

構造方法:

將物件寫入流的方法:

4、ObjectInputStream

構造方法:

要呼叫的方法:

簡單演示:一個物件的複製。
開發步驟:

  1. 建立ByteArrayOutputStream,將資料可以轉換成位元組
  2. 建立ObjectOutputStream,關聯ByteArrayOutputStream
  3. 使用ObjectOutputStream的writeObject方法,讀取要複製的物件
  4. 使用ByteArrayInputStream讀取ByteArrayOutputStream的轉化的物件位元組資料
  5. 建立ObjectInputStream並用readObject讀取物件位元組資料返回新物件
    素材User類:
package com.yynm.pojo;

import java.io.Serializable;

/**
 * @Description:
 * @Author: yeyulemon
 * @Date: 2021-12-19 20:09
 **/
public class User implements Serializable {
    private String username;
    private String 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;
    }
}

測試:

@Test
public void test06() throws Exception {
    User user1 = new User();
    user1.setUsername("張三");
    user1.setPassword("123456");

    // 1. 建立ByteArrayOutputStream,將資料可以轉換成位元組
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    // 2. 建立ObjectOutputStream,關聯ByteArrayOutputStream
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    // 3. 使用ObjectOutputStream的writeObject方法,讀取要複製的物件
    objectOutputStream.writeObject(user1);
    // 4. 使用ByteArrayInputStream讀取ByteArrayOutputStream的轉化的物件位元組資料
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    // 5. 建立ObjectInputStream並用readObject讀取物件位元組資料返回新物件
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    User user2 = (User) objectInputStream.readObject();

    System.out.println(user1 + ":" + user1.hashCode());
    System.out.println(user2 + ":" + user2.hashCode());
}

效果:

com.yynm.pojo.User@2b05039f:721748895
com.yynm.pojo.User@77468bd9:2001112025

4.3 使用IO改寫Person的clone方法

4.3.1 開發步驟

  1. 克隆涉及的所有的類實現Serializable
  2. 修改Person類的clone方法,使用IO複製物件
  3. 測試演示

4.3.2 程式碼實現

  1. 克隆涉及的所有的類實現Serializable
  2. 修改Person類的clone方法,使用IO複製物件
package com.yynm.pojo;

import java.io.*;

/**
 * @Description: Person實體類
 * @Author: yeyulemon
 * @Date: 2021-12-16 21:06
 **/
public class Person implements Cloneable, Serializable {
    private String name;
    private Integer age;
    private Children children;
    private Grandson grandson;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Children getChildren() {
        return children;
    }

    public void setChildren(Children children) {
        this.children = children;
    }

    public Grandson getGrandson() {
        return grandson;
    }

    public void setGrandson(Grandson grandson) {
        this.grandson = grandson;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        try {
            // 1. 建立ByteArrayOutputStream,將資料可以轉換成位元組
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 2. 建立ObjectOutputStream,關聯ByteArrayOutputStream
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            // 3. 使用ObjectOutputStream的writeObject方法,讀取要複製的物件
            oos.writeObject(this);
            // 4. 使用ByteArrayInputStream讀取ByteArrayOutputStream的轉化的物件位元組資料
            ByteArrayInputStream bAIS = new ByteArrayInputStream(baos.toByteArray());
            // 5. 建立ObjectInputStream並用readObject讀取物件位元組資料返回新物件
            ObjectInputStream oIS = new ObjectInputStream(bAIS);
            Person personClone = (Person) oIS.readObject();
            return personClone;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

測試:

@Test
public void test07() throws Exception {
    Person person1 = new Person();
    Person person2 = person1.clone();
    System.out.println(person1 + ":" + person1.hashCode());
    System.out.println(person2 + ":" + person2.hashCode());
}

效果:

com.yynm.pojo.Person@4dcbadb4:1305193908
com.yynm.pojo.Person@77468bd9:2001112025

5. 為什麼使用clone方法需要實現Cloneable介面

答:原始碼就是這麼設定的,實現介面僅僅是一個可以使用clone方法的標記。
那麼原始碼在哪裡設定的呢?
檢視jdk原始碼我們發現:

因此,我們需要檢視native修飾的背後的原始碼,這個一直要追溯到jdk底層C,C++原始碼。

5.1 下載完整jdk原始碼

下載地址:http://jdk.java.net/java-se-ri/7
檢視步驟



原始碼展示:

JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_Clone");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  const KlassHandle klass (THREAD, obj->klass());
  JvmtiVMObjectAllocEventCollector oam;

#ifdef ASSERT
  // Just checking that the cloneable flag is set correct
  if (obj->is_javaArray()) {
    guarantee(klass->is_cloneable(), "all arrays are cloneable");
  } else {
    guarantee(obj->is_instance(), "should be instanceOop");
    bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
    guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
  }
#endif

  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  // Make shallow object copy
  const int size = obj->size();
  oop new_obj = NULL;
  if (obj->is_javaArray()) {
    const int length = ((arrayOop)obj())->length();
    new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
  } else {
    new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
  }
  // 4839641 (4840070): We must do an oop-atomic copy, because if another thread
  // is modifying a reference field in the clonee, a non-oop-atomic copy might
  // be suspended in the middle of copying the pointer and end up with parts
  // of two different pointers in the field.  Subsequent dereferences will crash.
  // 4846409: an oop-copy of objects with long or double fields or arrays of same
  // won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
  // of oops.  We know objects are aligned on a minimum of an jlong boundary.
  // The same is true of StubRoutines::object_copy and the various oop_copy
  // variants, and of the code generated by the inline_native_clone intrinsic.
  assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                               (size_t)align_object_size(size) / HeapWordsPerLong);
  // Clear the header
  new_obj->init_mark();

  // Store check (mark entire object and let gc sort it out)
  BarrierSet* bs = Universe::heap()->barrier_set();
  assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
  bs->write_region(MemRegion((HeapWord*)new_obj, size));

  // Caution: this involves a java upcall, so the clone should be
  // "gc-robust" by this stage.
  if (klass->has_finalizer()) {
    assert(obj->is_instance(), "should be instanceOop");
    new_obj = instanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
  }

  return JNIHandles::make_local(env, oop(new_obj));
JVM_END

校驗當前類是否實現克隆介面的程式碼:

// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
if (!klass->is_cloneable()) {
  ResourceMark rm(THREAD);
  THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}

註釋翻譯:

陣列型別預設可以直接克隆,而其它物件實現clone需要先實現Cloneable介面,否則丟擲:
CloneNotSupportedException異常

結論,物件使用clone方法必須實現Cloneable介面。