克隆是创建对象副本的过程。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()
方法的运作情况。
emp 和 clonedEmp == test: false
: 这意味着 emp 和 clonedEmp 是两个不同的对象,而不是引用同一个对象。这符合 Java 对象克隆的要求。emp 和 clonedEmp HashMap == 测试: true
:因此 emp 和 clonedEmp 对象变量都引用同一对象。如果我们更改基础对象的值,这可能会导致严重的数据完整性问题。任何值的更改都可能会反映到克隆实例中。clonedEmp 属性:{城市=纽约,工资=10000,职称=CEO}
:我们没有对 clonedEmp 属性进行任何更改,但它们仍然发生了变化,因为 emp 和 clonedEmp 变量都引用同一对象。这是因为默认的 Object clone() 方法创建的是浅拷贝。当您想通过克隆过程创建完全分离的对象时,这可能会导致意外结果。因此需要正确地覆盖 Object clone() 方法。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对象克隆最佳实践
-
仅当你的类具有原始类型和不可变变量,或者你想要浅复制时,才使用默认的Object clone()方法。在继承的情况下,你将不得不检查所有你正在扩展的类,直到Object级别。
-
如果您的类主要包含可变属性,也可以定义复制构造函数。
-
通过在重写的克隆方法中调用
super.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仓库下载该项目。
Source:
https://www.digitalocean.com/community/tutorials/java-clone-object-cloning-java