Deep copy, shallow copy and clone, new method efficiency comparison

References

foreword

There are primitive types and reference types in Java. Assignment in Java is pass-by-value

  1. For basic types, the specific content is copied.
  2. For reference types, the stored value is only the address of the actual object, and copying will only copy the reference address.

On this basis, the "copy of the object" can be divided into two cases

  • shallow copy
    1. Pass-by-value for primitive data types
    2. Make a copy of the reference address for the reference data type
  • deep copy
    1. Pass-by-value for primitive data types
    2. For reference data types, create a new object and copy its contents

Copy related API

Object#clone

All objects in Java inherit from java.lang.Object. ObjectA method of protectedtype clone.

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

Object#clone()The method is nativeyes , so we don't need to implement it. It should be noted that the clonemethod is protectedyes , which means that the clonemethod is only visible in the java.langpackage or its subclasses.

This is not possible if we want to call a clonemethod . Because the clonemethods are defined in Object, the object has no externally visible clonemethods .

Cloneable interface

As mentioned above, Object#clone()methods are protectedyes , we cannot directly call clonemethods on an object in a program.

The JDK recommends "implementing the Cloneableinterface and overriding the clonemethod (using the publicmodifier ) ​​to implement a copy of the property".

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 {

}
复制代码

Reading the Cloneablesource code , there are the following conclusions

  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
复制代码

Finally, a conclusion is drawn on the " efficiency comparison of andclone "new

  • The JVM optimizes the way objects are created using the newmethod , and by default, newis more efficient.
  • newWhen an object is created by the method, the constructor of the class is called. If there is a time-consuming operation in the constructor, it will affect the efficiency of the newmethod to create the object.
  • clonemethod to create an object without calling the constructor of the class.

Based on the above conclusions, in the "Deep Copy" section above, the implementation of the deep copy function can be optimized, instead of calling the clonemethod to create the object, instead directly calling the constructor to implement it.

@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());
    }
}
复制代码

Guess you like

Origin juejin.im/post/7103385559623532558