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

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

基本代码

//代码清单1 Address.java
import 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.java
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public 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();
//代码清单3
public 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));
    }
}

output
person => 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一模一样

output
person => 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));
    }
}

output
person => 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深拷贝与浅拷贝
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值