Java中的clone()方法 – 在Java中进行克隆操作

克隆是创建对象的过程。Java中的Object类带有本地的clone()方法,该方法返回现有实例的副本。由于Object是Java中的基类,默认情况下所有对象都支持克隆。

Java对象克隆

java clone object
package com.Olivia.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.Olivia.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<String, String> props = new HashMap<>();
		props.put("salary", "10000");
		props.put("city", "Bangalore");
		emp.setProps(props);

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

		// Check whether the emp and clonedEmp attributes are same or different
		System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
		
		System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
		
		// Let's see the effect of using default cloning
		
		// change emp props
		emp.getProps().put("title", "CEO");
		emp.getProps().put("city", "New York");
		System.out.println("clonedEmp props:" + clonedEmp.getProps());

		// change emp name
		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

运行时的克隆不受支持异常

如果我们的员工类不实现Cloneable接口,上述程序将抛出一个在运行时抛出的CloneNotSupportedException异常。

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

了解对象克隆

让我们来看一下上面的输出并了解一下Object clone()方法的运作情况。

    1. emp和clonedEmp == test: false:这意味着emp和clonedEmp是两个不同的对象,不指向同一个对象。这符合Java对象克隆的要求。

 

    1. emp和clonedEmp HashMap == test: true:所以emp和clonedEmp对象变量都指向同一个对象。如果我们更改底层对象的值,这可能导致严重的数据完整性问题。任何值的更改可能会反映到克隆的实例中。

 

    1. clonedEmp props:{city=New York, salary=10000, title=CEO}:我们没有对clonedEmp属性进行任何更改,但它们仍然发生了变化,因为emp和clonedEmp变量都指向同一个对象。这是因为默认的Object clone()方法创建的是浅拷贝。当您希望通过克隆过程创建完全分离的对象时,这可能会成为一个问题。这可能导致不希望的结果,因此需要正确地重写Object clone()方法。

 

    clonedEmp name: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;

	// deep cloning for immutable fields
	emp.setProps(null);
	Map<String, String> hm = new HashMap<>();
	String key;
	Iterator<String> it = this.props.keySet().iterator();
	// Deep Copy of field by field
	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和克隆,在重写和处理可变字段时要明智地操作。如果你的类扩展了其他类,而这些类又扩展了其他类,以此类推,那可能是一项艰巨的任务。你必须一直沿着Object继承层级前进,以处理所有可变字段的深度复制。

使用序列化进行克隆?

在中文中,一种轻松执行深度克隆的方法是通过序列化。但是序列化是一个昂贵的过程,你的类应该实现Serializable接口。所有的字段和超类也必须实现Serializable。

使用Apache Commons Util

如果您的项目已经在使用Apache Commons Util类,并且您的类是可序列化的,那么请使用以下方法。

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

复制构造函数用于克隆

我们可以定义一个拷贝构造函数来创建对象的副本。为什么还要依赖于对象的clone()方法呢?例如,我们可以有一个类似下面代码示例的Employee拷贝构造函数。

public Employee(Employee emp) {
	
	this.setId(emp.getId());
	this.setName(emp.getName());
	
	Map<String, String> hm = new HashMap<>();
	String key;
	Iterator<String> it = emp.getProps().keySet().iterator();
	// Deep Copy of field by field
	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级别为止。

如果你的类主要具有可变属性,还可以定义复制构造函数。

通过在重写的克隆方法中调用super.clone()来利用Object clone()方法,然后对可变字段进行深度复制所需的更改。

如果你的类可序列化,可以使用序列化来进行克隆。然而,这可能会带来性能损失,所以在使用序列化进行克隆之前,请进行一些基准测试。

如果你扩展了一个类,并且它已经正确地使用深度复制定义了克隆方法,那么你可以利用默认的clone方法。例如,我们在Employee类中正确定义了clone()方法如下。

我们可以创建一个子类并利用父类的深度复制如下。

EmployeeWrap类没有可变属性,并且它正在利用父类的clone()方法实现。如果有可变字段,则只需要对这些字段进行深度复制。这是一个简单的程序来测试这种克隆方式是否正常工作。

所以它完全按照我们的期望工作。

这就是关于Java中的对象克隆。我希望你对Java对象的clone()方法以及如何正确地覆盖它没有任何不良影响有了一些了解。

你可以从我的 GitHub 代码库下载这个项目。

参考文档:Object clone 的 API 文档

发表回复 0

Your email address will not be published. Required fields are marked *


广告
将在 10 秒后关闭
bannerAds