Copia profunda, copia superficial y clonación, comparación de la eficiencia de nuevos métodos

Referencias

prefacio

Hay tipos primitivos y tipos de referencia en Java. La asignación en Java es pass-by-value

  1. Para los tipos básicos, se copia el contenido específico.
  2. 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
    1. Paso por valor para tipos de datos primitivos
    2. Haga una copia de la dirección de referencia para el tipo de datos de referencia
  • copia profunda
    1. Paso por valor para tipos de datos primitivos
    2. 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. ObjectSe proporciona un método de protectedtipo clone.

protected native Object clone() 
    throws CloneNotSupportedException;
复制代码

Object#clone()El método es nativeyes , por lo que no necesitamos implementarlo. Cabe señalar que el clonemétodo es protectedsí , lo que significa que el clonemétodo solo es visible en el java.langpaquete o sus subclases.

Esto no es posible si queremos llamar a un clonemétodo . Debido a que los clonemétodos están definidos en Object, el objeto no tiene clonemétodos .

Interfaz clonable

Como se mencionó anteriormente, Object#clone()los métodos son protectedsí , no podemos llamar directamente a clonemétodos en un objeto en un programa.

El JDK recomienda "implementar la Cloneableinterfaz y anular el clonemétodo (usando el publicmodificador ) 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 Cloneablecódigo fuente , hay las siguientes conclusiones.

  1. 对于实现 Cloneable 接口的对象,是可以调用 Object#clone() 方法来进行属性的拷贝。
  2. 若一个对象没有实现 Cloneable 接口,直接调用 Object#clone() 方法,会抛出 CloneNotSupportedException 异常。
  3. Cloneable 是一个空接口,并不包含 clone 方法。但是按照惯例(by convention),实现 Cloneable 接口时,应该以 public 修饰符重写 Object#clone() 方法(该方法在 Object 中是被 protected 修饰的)。

参照《Effective Java》中「第13条 谨慎地重写 clone 方法」

Cloneable 接口的目的是作为一个 mixin 接口,约定如果一个类实现了 Cloneable 接口,那么 Objectclone 方法将返回该对象的逐个属性(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 接口,那么 Objectclone 方法将返回该对象的逐个属性(field-by-field)拷贝,这里的拷贝是浅拷贝。

  1. 对基本数据类型进行值传递
  2. 对引用数据类型进行引用地址的拷贝

下面结合一个示例加以说明。

  • 定义两个对象,AddressCustomerUser
@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 的引用类型的属性值(AddressString[] 类型的属性值),会影响到原对象 customerUser

深拷贝

实现深拷贝有两种方式,「序列化对象方式」和「二次调用 clone 方式」

  1. 序列化(serialization)方式
    • 先对对象进行序列化,再进行反序列化,得到一个新的深拷贝的对象
  2. 二次调用 clone 方式
    • 先调用对象的 clone() 方法
    • 对对象的引用类型的属性值,继续调用 clone() 方法进行拷贝

下面,在「浅拷贝」章节示例的基础上,使用「二次调用 clone 方式」实现深拷贝。

  • 修改 CustomerUserclone() 方法,对 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":["奥迪","路虎"]}
复制代码
  • 可以看到 addresscars 是不同的,表示我们的深拷贝是成功的。

创建对象

在介绍 clone 方法的基础上,引出对「创建对象的4种方法」,「clone和new的效率对比」等问题的介绍。

创建对象的4种方法

创建对象的 4 种方法如下

  1. 使用 new 关键字
  2. 反射机制
  3. 实现 Cloneable 接口,使用 clone 方法创建对象
  4. 序列化和反序列化

以上 4 种方式,都可以创建一个 Java 对象,实现机制上有如下区别

  • 方式 1 和方式 2 中,都明确地显式地调用了对象的构造函数。
  • 方式 3 中,是对已经的对象,在内存上拷贝了一个影印,并不会调用对象的构造函数。
  • 方式 4 中,对对象进行序列化,转化为了一个文件流,再通过反序列化生成一个对象,也不会调用构造函数。

clone和new的效率对比

  • 使用 clone 创建对象,该操作并不会调用类的构造函数,是在内存中进行的数据块的拷贝,复制已有的对象。
  • 使用 new 方式创建对象,调用了类的构造函数。

使用 clone 创建对象,直接在内存中进行数据块的拷贝。这是否意味着 clone 方法的效率更高呢?

答案并不是,JVM 的开发者意识到通过 new 方式来生成对象的方式,使用的更加普遍,所以对于利用 new 操作生成对象进行了优化。

下面编写一个测试用例,用 clonenew 两种方式来创建 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 newmétodo y, de forma predeterminada, newes más eficiente.
  • newCuando 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 del newmétodo para crear el objeto.
  • clonemé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 clonemé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());
    }
}
复制代码

Supongo que te gusta

Origin juejin.im/post/7103385559623532558
Recomendado
Clasificación