【Java】Object的clone方法分析
學習視訊:https://study.163.com/course/introduction/1006177009.htm
學習目標
- 能夠理解clone方法的由來
- 能夠使用clone方法建立物件
- 能夠理解克隆物件和原物件的關係
- 能夠理解clone方法建立物件與new關鍵字和反射建立物件的不同
- 能夠理解淺表複製和深層複製的含義
- 能夠探尋物件的複製必須實現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 使用步驟
- 在需要clone方法的類上實現Cloneable介面
- 重寫clone方法,在自己的clone方法中呼叫父類的clone方法,將返回值型別強轉成本類型別,將當前clone方法修飾符改為public
- 在測試中呼叫物件的clone方法
1.2.2 程式碼演示
- 在需要clone方法的類上實現Cloneable介面
- 重寫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();
}
}
- 在測試中呼叫物件的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 開發步驟
- 修改Children類實現Cloneable介面
- 修改Children類重寫clone方法
- 修改Person類重寫clone方法,在clone方法中呼叫children的clone方法
3.2.2 程式碼實現
- 修改Children類實現Cloneable介面
- 修改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
構造方法:
要呼叫的方法:
簡單演示:一個物件的複製。
開發步驟:
- 建立ByteArrayOutputStream,將資料可以轉換成位元組
- 建立ObjectOutputStream,關聯ByteArrayOutputStream
- 使用ObjectOutputStream的writeObject方法,讀取要複製的物件
- 使用ByteArrayInputStream讀取ByteArrayOutputStream的轉化的物件位元組資料
- 建立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 開發步驟
- 克隆涉及的所有的類實現Serializable
- 修改Person類的clone方法,使用IO複製物件
- 測試演示
4.3.2 程式碼實現
- 克隆涉及的所有的類實現Serializable
- 修改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介面。