Java深(Deep)拷贝与浅(Shadow)拷贝

来源:互联网 发布:matlab 无标度网络 编辑:程序博客网 时间:2024/06/05 11:08

  • Java深Deep拷贝与浅Shadow拷贝
    • 基本代码
    • 浅拷贝
    • 深拷贝
    • 序列化实现深拷贝
      • 不足之处缺点局限性
    • 懒拷贝lazy copy
    • 总结

Java深(Deep)拷贝与浅(Shadow)拷贝

基本代码

//代码清单1 Address.javaimport lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor//浅拷贝,则Adress不需要实现Cloneable接口public class Address implements Cloneable {    private String country;/**国家*/    private String province;/**省*/    private String city;/**城市*/    public Address(String country,String province,String city){        this.country = country;        this.province = province;        this.city = city;    }    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }}
//代码清单2 Person.javaimport lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructorpublic class Person implements Cloneable {    private String name;    private String gender;    private Address address;    public Person(String name, String gender, Address address) {        this.name = name;        this.gender = gender;        this.address = address;    }    @Override    protected Object clone() throws CloneNotSupportedException {        /**         * 浅拷贝         */        //return super.clone();        /**         * 深拷贝         */        Person p = (Person)super.clone();        //把所有是引用类型的字段再拷贝一次        address = (Address) p.getAddress().clone();        return p;    }}

浅拷贝

浅拷贝(shadow copy)即按位拷贝对象。新对象通过精确拷贝源对象值的方式被创建出来。假如对象具有引用类型的字段,那么只会拷贝引用地址(内存地址;reference addresses or memory address)。

shadow_copy

如上图,结合代码清单1和2。在上图中person拥有字段name、gender和address,其中address是引用类型。对person进行浅拷贝后产生person2,person2仍然指向address。

经过观察发现,原始数据类型经过浅拷贝后,会生成新的一份数据,上图中是name->name1;gender->gender2。而address是一个Address对象,是引用类型,所以经过浅拷贝后仍旧会指向原来的address。

因此,person内的address经过任何改变,也会在person2中反映出来。

代码实现:

  1. 需要实现Cloneable接口。
  2. 重写clone()方法,返回super.clone();
//代码清单3public class Main {    public static void main(String[] args) throws CloneNotSupportedException{        /**待拷贝的源对象*/        Address address = new Address("中国","广西省","崇左市");        Person person = new Person("张三","男",address);        print("person => %s",person);        /**拷贝*/        Person person2 = (Person)person.clone();//拷贝效果看Person的clone()方法的内容。        print("person2 => %s",person2);        /**         * 测试浅/深拷贝特性:         * 1. 如果源拷贝对象有引用类型的字段,则改变引用类型的字段值,会既影响到源拷贝对象,又影响到clone后的对象         * 2. 改变原始类型字段值,则不会影响到双方         */        address.setProvince("湖南省");        address.setCity("长沙市");        person.setName("李四");        print("\n改变引用类型字段值后:");        print("person => %s",person);        print("person2 => %s",person2);    }    public static void print(String format,Object... args){        System.out.println(String.format(format,args));    }}outputperson => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))改变引用类型字段值后:person => Person(name=李四, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))person2 => Person(name=张三, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))

观察输出,浅拷贝结论是:

  1. clone重写只需要返回super.clone();
  2. 源对象和拷贝对象内的引用数据类型字段指向的都是同一个地方。即该字段在任意一个地方改变,都会如实反映在各个拷贝或源对象中。

深拷贝

深拷贝拷贝所有的字段(Fields),并且拷贝字段指向的动态分类的内存。当一个对象连同它指向的对象被拷贝时,深拷贝就发生了。

deep_copy

在上图中,person拥有字段name、gender、address。当对person进行深拷贝时,name1包含复制name的值,同理gender1和address1都包含复制的gender和address值。address发生任何改变,都不会在address1内发生改变。

代码实现:

  1. 需要实现Cloneable接口(源对象内的引用类型也许要实现Cloneable接口,并重写clone()方法)。
  2. 重写clone()方法。先调用要拷贝对象的clone()方法,然后在调用源对象内引用对象的clone()方法。
 1. 如代码清单2,下面是一些片段        /**         * 深拷贝         */        Person p = (Person)super.clone();        //把所有是引用类型的字段再拷贝一次        address = (Address) p.getAddress().clone();        return p;2. 测试代码和代码清单3一模一样outputperson => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))改变引用类型字段值后:person => Person(name=李四, gender=男, address=Address(country=中国, province=广西省, city=崇左市))person2 => Person(name=张三, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))

深拷贝结论:

  1. 源对象和拷贝对象之间互不影响。无论字段是否是应用类型还是原始数据类型。

序列化实现深拷贝

注意,对象内的所有类都必须实现序列化接口。

1. 代码清单12中   Address.java 、Person.java需要实现Serializable接口//测试代码如下public class Main {    public static void main(String[] args) throws IOException {        ObjectOutputStream oos = null;        ObjectInputStream ois = null;        try {            // create original serializable object            Address address = new Address("中国", "广西省", "崇左市");            Person person = new Person("张三", "男", address);            // print it            print("person => %s", person);            // deep copy            ByteArrayOutputStream bos = new ByteArrayOutputStream();            oos = new ObjectOutputStream(bos);            // serialize and pass the object            oos.writeObject(person);            oos.flush();            ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());            ois = new ObjectInputStream(bin);            // return the new object            Person person2 = (Person) ois.readObject();            // verify it is the same            print("person2 => %s", person2);            // change the original object's contents            address.setProvince("湖南省");            address.setCity("长沙市");            person.setName("李四");            // see what is in each one now            print("\n改变引用类型字段值后:");            print("person => %s", person);            print("person2 => %s", person2);        } catch (Exception e) {            System.out.println("Exception in main = " + e);        } finally {            oos.close();            ois.close();        }    }    public static void print(String format,Object... args){        System.out.println(String.format(format,args));    }}outputperson => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))改变引用类型字段值后:person => Person(name=李四, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))

不足之处(缺点、局限性)

  1. 不能序列化一个transient变量。
  2. 如果是单例模式,你以这种方式进行深拷贝,则它就不再是单例模式了。
  3. 性能问题。创建一个socket,序列化一个对象,然后通过socket传递它,最后反序列化。不建议这样用,因为它比实现Cloneable慢100倍左右。

懒拷贝(lazy copy)

懒拷贝实际上是深拷贝和浅拷贝的结合体。当初始化的时候,用浅拷贝。一个计数器需要用来追踪有多少个对象共享数据,当应用更改源对象时,它知道数据是否共享,并决定是否要做深拷贝。

懒拷贝从结果看,像极了深拷贝,只是它充分利用了浅拷贝的优势——速度快。当源对象内的引用数据类型字段不经常被更改时,用浅拷贝

缺点是追踪counter的花销,此外,在特殊情况下,循环引用也会导致问题。

总结

使用场景有:

  1. 使用浅拷贝:对象只有原始数据类型字段;有引用类型的字段,但从不更改它。

  2. 使用深拷贝:有引用类型的字段,且经常被修改。

简言之,使用哪种拷贝方式看需求如何。

其实也可以手动进行深拷贝,或者浅拷贝,即

import org.springframework.beans.BeanUtils//Copy the property values of the given source bean into the target bean.    public static void copyProperties(Object source, Object target) throws BeansException {        copyProperties(source, target, null, (String[]) null);    }

参考来源

  1. Java深拷贝与浅拷贝
  2. Java深拷贝与浅拷贝
  3. Java深拷贝与浅拷贝