ディープコピー、シャローコピーとクローン、新しいメソッドの効率比較

参考文献

序文

Javaにはプリミティブ型と参照型があります。Javaでの代入は値渡しです

  1. 基本タイプの場合、特定のコンテンツがコピーされます。
  2. 参照型の場合、格納される値は実際のオブジェクトのアドレスのみであり、コピーすると参照アドレスのみがコピーされます。

これに基づいて、「オブジェクトのコピー」は2つのケースに分けることができます

  • 浅いコピー
    1. プリミティブデータ型の値渡し
    2. 参照データ型の参照アドレスのコピーを作成します
  • ディープコピー
    1. プリミティブデータ型の値渡し
    2. 参照データ型については、新しいオブジェクトを作成し、その内容をコピーします

関連するAPIをコピーする

Object#clone

Javaのすべてのオブジェクトはから継承しjava.lang.Objectます。タイプObjectのメソッドがオブジェクトに提供されますprotectedclone

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

Object#clone()メソッドはnativeyesなので、実装する必要はありません。cloneメソッドがprotectedyesであることに注意してください。これは、cloneメソッドがjava.langパッケージことを意味します。

プログラム内のオブジェクトのcloneメソッドこれは不可能です。cloneメソッドはで定義されているためObject、オブジェクトには外部から見えるcloneメソッド。

クローン可能なインターフェース

上記のように、Object#clone()メソッドはprotectedyescloneです。プログラム内のオブジェクトに対して、メソッドを直接呼び出すことはできません。

JDKは、「Cloneableインターフェース、cloneメソッドをオーバーライドして(public修飾子)プロパティのコピーを実装する」ことを推奨しています。

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 {

}
复制代码

Cloneableソースコードを読むと、次の結論があります

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

最後に、「の効率比較」について結論が導き出されます。clonenew

  • JVMは、メソッドを使用してオブジェクトを作成するnew方法デフォルトでnewはより効率的です。
  • newメソッドによってオブジェクトが作成されると、クラスのコンストラクターが呼び出されます。コンストラクターに時間のかかる操作がある場合、オブジェクトnewを作成するメソッドます。
  • cloneクラスのコンストラクターを呼び出さずにオブジェクトを作成するメソッド。

上記の結論に基づいて、上記の「ディープコピー」セクションでは、オブジェクトを作成するcloneメソッド。

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

おすすめ

転載: juejin.im/post/7103385559623532558