Referencias
- Java efectivo, "Ítem 13: anule el método de clonación con prudencia"
- Comprensión profunda de Java de copia superficial y copia profunda | Nuggets
- Elaborar una copia profunda y una copia superficial de Java | Segmentfault
prefacio
Hay tipos primitivos y tipos de referencia en Java. La asignación en Java es pass-by-value
- Para los tipos básicos, se copia el contenido específico.
- Para los tipos de referencia, el valor almacenado es solo la dirección del objeto real y la copia solo copiará la dirección de referencia.
Sobre esta base, la "copia del objeto" se puede dividir en dos casos
- copia superficial
- Paso por valor para tipos de datos primitivos
- Haga una copia de la dirección de referencia para el tipo de datos de referencia
- copia profunda
- Paso por valor para tipos de datos primitivos
- Para tipos de datos de referencia, cree un nuevo objeto y copie su contenido
Copiar API relacionada
Objeto#clonar
Todos los objetos en Java heredan de java.lang.Object
. Object
Se proporciona un método de protected
tipo clone
.
protected native Object clone()
throws CloneNotSupportedException;
复制代码
Object#clone()
El método es native
yes , por lo que no necesitamos implementarlo. Cabe señalar que el clone
método es protected
sí , lo que significa que el clone
método solo es visible en el java.lang
paquete o sus subclases.
Esto no es posible si queremos llamar a un clone
método . Debido a que los clone
métodos están definidos en Object
, el objeto no tiene clone
métodos .
Interfaz clonable
Como se mencionó anteriormente, Object#clone()
los métodos son protected
sí , no podemos llamar directamente a clone
métodos en un objeto en un programa.
El JDK recomienda "implementar la Cloneable
interfaz y anular el clone
método (usando el public
modificador ) para implementar una copia de la propiedad".
package java.lang;
/**
* 此处给出 Cloneable 的部分注释
* A class implements the Cloneable interface to
* indicate to the java.lang.Object#clone() method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
*
* Invoking Object's clone method on an instance that does not implement the
* Cloneable interface results in the exception
* CloneNotSupportedException being thrown.
*
* By convention, classes that implement this interface should override
* Object.clone (which is protected) with a public method.
*/
public interface Cloneable {
}
复制代码
Leyendo el Cloneable
código fuente , hay las siguientes conclusiones.
- 对于实现
Cloneable
接口的对象,是可以调用Object#clone()
方法来进行属性的拷贝。 - 若一个对象没有实现
Cloneable
接口,直接调用Object#clone()
方法,会抛出CloneNotSupportedException
异常。 Cloneable
是一个空接口,并不包含clone
方法。但是按照惯例(by convention
),实现Cloneable
接口时,应该以public
修饰符重写Object#clone()
方法(该方法在Object
中是被protected
修饰的)。
参照《Effective Java》中「第13条 谨慎地重写 clone 方法」
Cloneable
接口的目的是作为一个mixin
接口,约定如果一个类实现了Cloneable
接口,那么Object
的clone
方法将返回该对象的逐个属性(field-by-field
)拷贝(浅拷贝);否则会抛出CloneNotSupportedException
异常。
上面第1、2点,可用下面的伪代码描述。
protected Object clone throws CloneNotSupportedException {
if(!(this instanceof Cloneable)){
throw new CloneNotSupportedException("Class" + getClass().getName() + "doesn't implement Cloneable");
}
return internalClone();
}
/**
* Native Helper method for cloning.
*/
private native Object internalClone();
复制代码
浅拷贝
参考 Cloneable
接口的源码注释部分,如果一个类实现了 Cloneable
接口,那么 Object
的 clone
方法将返回该对象的逐个属性(field-by-field
)拷贝,这里的拷贝是浅拷贝。
- 对基本数据类型进行值传递
- 对引用数据类型进行引用地址的拷贝
下面结合一个示例加以说明。
- 定义两个对象,
Address
和CustomerUser
@Data
class Address implements Cloneable{
private String name;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
class CustomerUser implements Cloneable{
private String firstName;
private String lastName;
private Address address;
private String[] cars;
@Override
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
复制代码
- 在测试方法
testShallowCopy
中,使用customerUser.clone()
进行对象拷贝。注意,此处为浅拷贝。
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
testShallowCopy();
}
public static void testShallowCopy() throws CloneNotSupportedException {
Address address= new Address();
address.setName("北京天安门");
CustomerUser customerUser = new CustomerUser();
customerUser.setAddress(address);
customerUser.setLastName("李");
customerUser.setFirstName("雷");
String[] cars = new String[]{"别克","路虎"};
customerUser.setCars(cars);
//浅拷贝
CustomerUser customerUserCopy =(CustomerUser) customerUser.clone();
customerUserCopy.setFirstName("梅梅");
customerUserCopy.setLastName("韩");
customerUserCopy.getAddress().setName("北京颐和园");
customerUserCopy.getCars()[0]="奥迪";
System.out.println("customerUser: " + JSONUtil.toJsonStr(customerUser));
System.out.println("customerUserCopy: " + JSONUtil.toJsonStr(customerUserCopy));
}
}
复制代码
- 程序运行结果如下。
customerUser: {"lastName":"李","address":{"name":"北京颐和园"},"firstName":"雷","cars":["奥迪","路虎"]}
customerUserCopy: {"lastName":"韩","address":{"name":"北京颐和园"},"firstName":"梅梅","cars":["奥迪","路虎"]}
复制代码
- 可以看到,修改拷贝之后的
customerUserCopy
的引用类型的属性值(Address
和String[]
类型的属性值),会影响到原对象customerUser
。
深拷贝
实现深拷贝有两种方式,「序列化对象方式」和「二次调用 clone
方式」
- 序列化(
serialization
)方式- 先对对象进行序列化,再进行反序列化,得到一个新的深拷贝的对象
- 二次调用
clone
方式- 先调用对象的
clone()
方法 - 对对象的引用类型的属性值,继续调用
clone()
方法进行拷贝
- 先调用对象的
下面,在「浅拷贝」章节示例的基础上,使用「二次调用 clone
方式」实现深拷贝。
- 修改
CustomerUser
的clone()
方法,对CustomerUser
对象的引用类型的属性值,即Address
属性值和数组(String[]
)属性值cars
,二次调用clone
方法。
@Data
class CustomerUser implements Cloneable{
private String firstName;
private String lastName;
private Address address;
private String[] cars;
@Override
public Object clone() throws CloneNotSupportedException{
CustomerUser customerUserDeepCopy = (CustomerUser) super.clone();
//二次调用clone方法
customerUserDeepCopy.address = (Address) address.clone();
customerUserDeepCopy.cars = cars.clone();
return customerUserDeepCopy;
}
}
复制代码
- 再次运行程序,输出结果如下。
customerUser: {"lastName":"李","address":{"name":"北京天安门"},"firstName":"雷","cars":["别克","路虎"]}
customerUserCopy: {"lastName":"韩","address":{"name":"北京颐和园"},"firstName":"梅梅","cars":["奥迪","路虎"]}
复制代码
- 可以看到
address
和cars
是不同的,表示我们的深拷贝是成功的。
创建对象
- ref 1-Java中的clone和new的效率对比
在介绍 clone
方法的基础上,引出对「创建对象的4种方法」,「clone和new的效率对比」等问题的介绍。
创建对象的4种方法
创建对象的 4 种方法如下
- 使用
new
关键字 - 反射机制
- 实现
Cloneable
接口,使用clone
方法创建对象 - 序列化和反序列化
以上 4 种方式,都可以创建一个 Java 对象,实现机制上有如下区别
- 方式 1 和方式 2 中,都明确地显式地调用了对象的构造函数。
- 方式 3 中,是对已经的对象,在内存上拷贝了一个影印,并不会调用对象的构造函数。
- 方式 4 中,对对象进行序列化,转化为了一个文件流,再通过反序列化生成一个对象,也不会调用构造函数。
clone和new的效率对比
- 使用
clone
创建对象,该操作并不会调用类的构造函数,是在内存中进行的数据块的拷贝,复制已有的对象。 - 使用
new
方式创建对象,调用了类的构造函数。
使用 clone
创建对象,直接在内存中进行数据块的拷贝。这是否意味着 clone
方法的效率更高呢?
答案并不是,JVM 的开发者意识到通过 new
方式来生成对象的方式,使用的更加普遍,所以对于利用 new
操作生成对象进行了优化。
下面编写一个测试用例,用 clone
和 new
两种方式来创建 10000 * 1000
个对象,测试对应的耗时。
public class Bean implements Cloneable {
private String name;
public Bean(String name) {
this.name = name;
}
@Override
protected Bean clone() throws CloneNotSupportedException {
return (Bean) super.clone();
}
}
public class TestClass {
private static final int COUNT = 10000 * 1000;
public static void main(String[] args) throws CloneNotSupportedException {
long s1 = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
Bean bean = new Bean("ylWang");
}
long s2 = System.currentTimeMillis();
Bean bean = new Bean("ylWang");
for (int i = 0; i < COUNT; i++) {
Bean b = bean.clone();
}
long s3 = System.currentTimeMillis();
System.out.println("new = " + (s2 - s1));
System.out.println("clone = " + (s3 - s2));
}
}
复制代码
程序输出如下。
new = 7
clone = 83
复制代码
可以看到,创建 10000 * 1000
个对象,使用 new
方法的耗时,只有 clone
方式的 1/10,即 new
方式创建对象的效率更高。
但是,若构造函数中有一些耗时操作,则 new
方式创建对象的效率会受到构造函数性能的影响。如下代码,在构造函数中添加字符串截取的耗时操作。
public class Bean implements Cloneable {
private String name;
private String firstSign;//获取名字首字母
public Bean(String name) {
this.name = name;
if (name.length() != 0) {
firstSign = name.substring(0, 1);
firstSign += "abc";
}
}
@Override
protected Bean clone() throws CloneNotSupportedException {
return (Bean) super.clone();
}
}
复制代码
此时,再执行测试用例,创建 10000 * 1000
个对象,程序输出如下,使用 new
方法的耗时,就远大于 clone
方式了。
new = 7
clone = 83
复制代码
Finalmente, se extrae una conclusión sobre la " comparación de eficiencia de yclone
"new
- La JVM optimiza la forma en que se crean los objetos utilizando el
new
método y, de forma predeterminada,new
es más eficiente. new
Cuando el método crea un objeto, se llama al constructor de la clase. Si hay una operación que requiere mucho tiempo en el constructor, afectará la eficiencia delnew
método para crear el objeto.clone
método para crear un objeto sin llamar al constructor de la clase.
Según las conclusiones anteriores, en la sección anterior "Copia profunda", se puede optimizar la implementación de la función de copia profunda, en lugar de llamar al clone
método para crear el objeto, en lugar de llamar directamente al constructor para implementarlo.
@Data
class Address implements Cloneable{
private String name;
Address(Address address){
this.name=address.name;
}
}
@Data
class CustomerUser implements Cloneable{
private String firstName;
private String lastName;
private Address address;
private String[] cars;
CustUserDeep(CustUserDeep custUserDeep){
this.firstName = custUserDeep.firstName;
this.lastName = custUserDeep.lastName;
this.cars = custUserDeep.getCars().clone();
this.address = new Address(custUserDeep.getAddress());
}
}
复制代码