Java Object clone() 方法 – 在 Java 中進行克隆

克隆是創建對象副本的過程。 Java Object類帶有本機 clone()方法,該方法返回現有實例的副本。由於Object是Java中的基類,所有對象默認支持克隆。

Java對象克隆

如果您想使用Java對象clone()方法,您必須實現 java.lang.Cloneable標記界面。否則,它將在運行時拋出 CloneNotSupportedException。還有Object clone是一個受保護的方法,所以您將不得不覆蓋它。讓我們通過一個示例程序來看看Java中的對象克隆。

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Employee implements Cloneable {

	private int id;

	private String name;

	private Map<String, String> props;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

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

	public Map<String, String> getProps() {
		return props;
	}

	public void setProps(Map<String, String> p) {
		this.props = p;
	}

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

}

我們正在使用Object clone()方法,因此我們已經實現了 Cloneable 界面。我們正在調用超類的clone()方法,即Object clone()方法。

使用Object clone()方法

讓我們創建一個測試程式,使用對象的 `clone()` 方法來創建該實例的複製。

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Map;

public class CloningTest {

	public static void main(String[] args) throws CloneNotSupportedException {

		Employee emp = new Employee();

		emp.setId(1);
		emp.setName("Pankaj");
		Map props = new HashMap<>();
		props.put("salary", "10000");
		props.put("city", "Bangalore");
		emp.setProps(props);

		Employee clonedEmp = (Employee) emp.clone();

		// 檢查 emp 和 clonedEmp 屬性是否相同或不同
		System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
		
		System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
		
		// 讓我們看看使用默認克隆的效果
		
		// 更改 emp 屬性
		emp.getProps().put("title", "CEO");
		emp.getProps().put("city", "New York");
		System.out.println("clonedEmp props:" + clonedEmp.getProps());

		// 更改 emp 名稱
		emp.setName("new");
		System.out.println("clonedEmp name:" + clonedEmp.getName());

	}

}

輸出:

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: true
clonedEmp props:{city=New York, salary=10000, title=CEO}
clonedEmp name:Pankaj

在運行時的 CloneNotSupportedException

如果我們的 Employee 類不實現 Cloneable 接口,上述程序將在運行時拋出 CloneNotSupportedException 錯誤例外

Exception in thread "main" java.lang.CloneNotSupportedException: com.journaldev.cloning.Employee
	at java.lang.Object.clone(Native Method)
	at com.journaldev.cloning.Employee.clone(Employee.java:41)
	at com.journaldev.cloning.CloningTest.main(CloningTest.java:19)

理解對象克隆

讓我們查看上面的輸出,了解對象 clone() 方法的運作。

  1. emp 和 clonedEmp == 測試:false:這意味著 emp 和 clonedEmp 是兩個不同的對象,不是指向同一個對象。這符合 Java 對象克隆的要求。
  2. emp 和 clonedEmp HashMap == 測試: 真:所以 emp 和 clonedEmp 物件變數都參考同一個物件。如果我們改變基礎物件的值,這可能是一個嚴重的資料完整性問題。任何值的變更都可能會反映到複製的實例中。
  3. clonedEmp 屬性:{city=紐約, salary=10000, title=CEO}:我們沒有對 clonedEmp 屬性進行任何更改,但它們仍然被更改,因為 emp 和 clonedEmp 變數都參考同一個物件。這是因為默認的 Object clone() 方法創建了一個淺層拷貝。當您希望通過克隆過程創建完全分離的物件時,這可能是一個問題。這可能導致不需要的結果,因此需要適當地覆寫 Object clone() 方法。
  4. clonedEmp 名稱:Pankaj:這裡發生了什麼?我們改變了 emp 的名稱,但 clonedEmp 的名稱沒有變。這是因為 String 是不可變的。所以當我們設置 emp 的名稱時,會創建一個新的字符串,並且 emp 的名稱引用在 this.name = name; 中更改。因此 clonedEmp 的名稱保持不變。對於任何原始變數類型,您也會發現類似的行為。只要物件中只有原始和不可變變數,我們就可以使用 java 物件的默認克隆。

物件克隆類型

有兩種類型的對象克隆 – 淺克隆和深克隆。讓我們了解它們的每個並找出在我們的Java程序中實現克隆的最佳方法。

1. 淺克隆

Java Object clone() 方法的默認實現使用淺拷貝。它使用反射API來創建實例的拷貝。下面的代碼片段展示了淺拷貝的實現。

@Override
 public Object clone() throws CloneNotSupportedException {
 
	 Employee e = new Employee();
	 e.setId(this.id);
	 e.setName(this.name);
	 e.setProps(this.props);
	 return e;
}

2. 深克隆

在深克隆中,我們必須逐個複製字段。如果我們有一個包含嵌套對象(例如列表、映射等)的字段,那麼我們也必須逐個編寫代碼來複製它們。這就是為什麼它被稱為深克隆或深拷貝。我們可以像下面的代碼一樣覆蓋 Employee clone 方法來實現深克隆。

public Object clone() throws CloneNotSupportedException {

	Object obj = super.clone(); //utilize clone Object method

	Employee emp = (Employee) obj;

	// 不可變字段的深克隆
	emp.setProps(null);
	Map hm = new HashMap<>();
	String key;
	Iterator it = this.props.keySet().iterator();
	// 逐字段深拷貝
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, this.props.get(key));
	}
	emp.setProps(hm);
	
	return emp;
}

使用這個 clone() 方法實現,我們的測試程序將產生以下輸出。

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: false
clonedEmp props:{city=Bangalore, salary=10000}
clonedEmp name:Pankaj

在大多數情況下,這是我們想要的。clone()方法應該返回一個完全與原始實例分離的新對象。所以如果你打算在你的程序中使用Object clone和cloning,請明智地使用並適當地覆蓋它,並注意可變字段。如果你的類擴展了其他類,而這些類又擴展了其他類,這可能是一個令人生畏的任務。你將不得不遍歷整個Object繼承層次結構,為所有可變字段進行深拷貝。

使用序列化進行克隆?

一種簡單實現深拷貝的方式是通過序列化。但是序列化是一個耗時的過程,你的類應該實現Serializable接口。所有的字段和父類也必須實現Serializable接口。

使用Apache Commons Util

如果你已經在項目中使用Apache Commons Util類,並且你的類是可序列化的,那麼可以使用以下方法。

Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);

複製用於克隆的複製構造函數

我們可以定義一個複製構造函數來創建對象的副本。為什麼要完全依賴於Object的clone()方法呢?例如,我們可以像以下代碼一樣擁有一個Employee複製構造函數。

public Employee(Employee emp) {
	
	this.setId(emp.getId());
	this.setName(emp.getName());
	
	Map hm = new HashMap<>();
	String key;
	Iterator it = emp.getProps().keySet().iterator();
	// 逐個字段進行深拷貝
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, emp.getProps().get(key));
	}
	this.setProps(hm);

}

每當我們需要員工對象的副本時,我們可以使用Employee clonedEmp = new Employee(emp);來獲取。但是如果你的類有很多變量,特別是基本類型和不可變類型,寫複製構造函數可能會是一件繁瑣的工作。

Java對象克隆的最佳實踐

  1. 只有當你的類具有基本類型和不可變變量,或者你想要淺拷貝時,才使用默認的Object clone()方法。在繼承的情況下,你將不得不檢查所有你正在擴展的類,直到Object級別。

  2. 如果您的類別主要具有可變屬性,您也可以定義複製構造函數。

  3. 通過在覆蓋的複製方法中調用super.clone(),然後對可變字段進行必要的更改,利用Object clone()方法來實現深層複製

  4. 如果您的類別是可序列化的,您可以使用序列化進行克隆。但是,這將導致性能下降,因此在使用序列化進行克隆之前,請先進行一些基準測試。

  5. 如果您正在擴展一個類,並且它已經正確地使用深拷貝定義了clone方法,那麼您可以利用默認的clone方法。例如,我們在Employee類中正確定義了clone()方法如下。

    @Override
    public Object clone() throws CloneNotSupportedException {
    
    	Object obj = super.clone();
    
    	Employee emp = (Employee) obj;
    
    	// 對於不可變字段的深層克隆
    	emp.setProps(null);
    	Map<String, String> hm = new HashMap<>();
    	String key;
    	Iterator<String> it = this.props.keySet().iterator();
    	// 逐個字段進行深拷貝
    	while (it.hasNext()) {
    		key = it.next();
    		hm.put(key, this.props.get(key));
    	}
    	emp.setProps(hm);
    
    	return emp;
    }
    

    我們可以創建一個子類並利用超類的深克隆,如下所示。

    package com.journaldev.cloning;
    
    public class EmployeeWrap extends Employee implements Cloneable {
    
    	private String title;
    
    	public String getTitle() {
    		return title;
    	}
    
    	public void setTitle(String t) {
    		this.title = t;
    	}
    
    	@Override
    	public Object clone() throws CloneNotSupportedException {
    
    		return super.clone();
    	}
    }
    

    EmployeeWrap類沒有任何可變屬性,並且它正在使用超類的clone()方法實現。如果存在可變字段,則必須謹慎處理僅這些字段的深拷貝。這裡是一個簡單的程序,用來測試這種克隆方式是否正常工作。

    package com.journaldev.cloning;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class CloningTest {
    
    	public static void main(String[] args) throws CloneNotSupportedException {
    
    		EmployeeWrap empWrap = new EmployeeWrap();
    
    		empWrap.setId(1);
    		empWrap.setName("Pankaj");
    		empWrap.setTitle("CEO");
    		
    		Map<String, String> props = new HashMap<>();
    		props.put("salary", "10000");
    		props.put("city", "Bangalore");
    		empWrap.setProps(props);
    
    		EmployeeWrap clonedEmpWrap = (EmployeeWrap) empWrap.clone();
    		
    		empWrap.getProps().put("1", "1");
    		
    		System.out.println("empWrap mutable property value = "+empWrap.getProps());
    
    		System.out.println("clonedEmpWrap mutable property value = "+clonedEmpWrap.getProps());
    		
    	}
    
    }
    

    輸出:

    empWrap mutable property value = {1=1, city=Bangalore, salary=10000}
    clonedEmpWrap mutable property value = {city=Bangalore, salary=10000}
    

    所以它完全按照我們的預期工作。

這就是Java中對象克隆的全部內容。我希望您對Java對象clone()方法以及如何正確覆蓋它而不產生任何負面影響有了一些了解。

您可以從我的 GitHub存儲庫 下載該項目。

參考:Object clone的API文檔

Source:
https://www.digitalocean.com/community/tutorials/java-clone-object-cloning-java