Java Object clone()方法 – Java中的克隆

克隆是创建对象副本的过程。Java对象类带有原生clone()方法,该方法返回现有实例的副本。由于对象是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 == test: false: 这意味着 emp 和 clonedEmp 是两个不同的对象,而不是引用同一个对象。这符合 Java 对象克隆的要求。
  2. emp 和 clonedEmp HashMap == 测试: true:因此 emp 和 clonedEmp 对象变量都引用同一对象。如果我们更改基础对象的值,这可能会导致严重的数据完整性问题。任何值的更改都可能会反映到克隆实例中。
  3. clonedEmp 属性:{城市=纽约,工资=10000,职称=CEO}:我们没有对 clonedEmp 属性进行任何更改,但它们仍然发生了变化,因为 emp 和 clonedEmp 变量都引用同一对象。这是因为默认的 Object clone() 方法创建的是浅拷贝。当您想通过克隆过程创建完全分离的对象时,这可能会导致意外结果。因此需要正确地覆盖 Object clone() 方法。
  4. clonedEmp 名称:Pankaj:这里发生了什么?我们更改了 emp 的名称,但 clonedEmp 的名称没有更改。这是因为 String 是不可变的。因此,当我们设置 emp 的名称时,会创建一个新的字符串,并且在 this.name = name; 中更改 emp 的名称引用。因此 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. 深克隆

在深克隆中,我们必须逐个复制字段。如果我们有一个包含嵌套对象(如List、Map等)的字段,那么我们必须逐个编写代码来复制它们。这就是为什么它被称为深克隆。我们可以像以下代码一样重写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对象的副本时,可以使用Employee clonedEmp = new Employee(emp);。然而,如果你的类有很多变量,特别是原始类型和不可变类型,编写复制构造函数可能会很繁琐。

Java对象克隆最佳实践

  1. 仅当你的类具有原始类型和不可变变量,或者你想要浅复制时,才使用默认的Object clone()方法。在继承的情况下,你将不得不检查所有你正在扩展的类,直到Object级别。

  2. 如果您的类主要包含可变属性,也可以定义复制构造函数。

  3. 通过在重写的克隆方法中调用 super.clone() 方法来利用对象的 clone() 方法,然后对可变字段进行必要的更改,以实现深复制

  4. 如果您的类可序列化,您可以使用序列化来进行克隆。但是,这将带来性能损耗,因此在使用序列化进行克隆之前,请先进行一些基准测试。

  5. 如果您正在扩展一个类,并且它已经正确地使用深度复制定义了克隆方法,那么您可以利用默认的克隆方法。例如,我们已经在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仓库下载该项目。

参考:对象clone的API文档

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