深拷贝&浅拷贝

        在java中我们创建对象可以通过new的方式创建,但是除了这种最常见的方式外,我们也可以通过反射、clone方法和反序列化的方式来创建对象。本文主要讲解clone方法和序列化这两种。

        使用clone方法需要实现Cloneable接口和覆写clone方法。需要注意的是:用clone方法创建对象并不会调用构造器。

public class Person implements Cloneable {

    private String name;
    private int    age;
    private Phone  phone;

    public Person(String name, int age, Phone phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    public Person() {}

    private class Phone {

        private String type;

        private Phone(String type) {
            this.type = type;
        }

        private String getType() {
            return type;
        }

        private void setType(String type) {
            this.type = type;
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Phone getPhone() {
        return phone;
    }

    public void setPhone(Phone phone) {
        this.phone = phone;
    }

    @Override
    public Person clone() {
        Person person = null;
        try {
            person = (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
        return person;
    }

    public static void main(String[] args) {
        Person man = new Person("Robert Hou", 23, new Person().new Phone("iPhone 8 Plus"));
        Person cloneMan = man.clone();
        System.out.println("man: " + man);
        System.out.println("cloneMan: " + cloneMan);
        System.out.println("man == cloneMan: " + (man == cloneMan));
        System.out.println("man.getPhone() == cloneMan.getPhone(): " + (man.getPhone() == cloneMan.getPhone()));
    }
}

        如上所示:覆写clone方法时可以将访问限制符改为public,这样可以确保任何类都能调用clone方法。运行结果如下:

man: com.hys.test.Person@7852e922
cloneMan: com.hys.test.Person@4e25154f
man == cloneMan: false
man.getPhone() == cloneMan.getPhone(): true

        可以看到,两个对象是存在不同的内存地址中,所以是false。但是从最后一行结果可知:Person类内的Phone却引用了同一个对象。所以如果类中的属性是引用类型,则引用类型的拷贝是拷贝的引用,而不是值。即clone方法是浅拷贝的,这点需要特别注意。深拷贝和浅拷贝的区别如下图所示(String常量池在jdk7之前是存在永久代(PermGen)中,但从jdk7开始进行去永久代的工作,常量池也随即放在了堆中,在jdk8中完全去掉了永久代的概念。详情可以查看笔者的另一篇博客《JVM总结》):


        那么浅拷贝会带来什么问题呢?考虑下面的代码:

        Person man = new Person("Robert Hou", 23, new Person().new Phone("iPhone 8 Plus"));
        Person cloneMan = man.clone();
        man.getPhone().setType("iPhone X");
        System.out.println("man.getPhone().getType(): " + man.getPhone().getType());
        System.out.println("cloneMan.getPhone().getType(): " + cloneMan.getPhone().getType());

        运行结果:

man.getPhone().getType(): iPhone X
cloneMan.getPhone().getType(): iPhone X

        当我去修改原对象的引用对象属性值的时候,发现克隆对象的值也跟着一起被修改了。这就是引用了同一个对象所导致的问题,并没有做到完全的隔离。

        如果想要做到完全的隔离,也就是深拷贝,需要将Phone内部类也实现Cloneable接口和覆写clone方法,Phone内部类修改如下:

private class Phone implements Cloneable {

        private String type;

        private Phone(String type) {
            this.type = type;
        }

        private String getType() {
            return type;
        }

        private void setType(String type) {
            this.type = type;
        }

        @Override
        public Phone clone() {
            Phone head = null;
            try {
                head = (Phone) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException(e);
            }
            return head;
        }
    }

        同时,在Person类的clone方法里也要调用Phone的clone方法,如下:

    @Override
    public Person clone() {
        Person person = null;
        try {
            person = (Person) super.clone();
            person.phone = (Phone) phone.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
        return person;
    }

        此时再次运行测试方法,得到正确的结果:

man.getPhone().getType(): iPhone X
cloneMan.getPhone().getType(): iPhone 8 Plus

        这种方式虽然能实现深拷贝,但是对于类中内部属性来说,都要判断是否是引用类型,然后实现Cloneable接口和覆写clone方法。这对于简单的类属性还好,但是如果类中加入了很多复杂的内部类属性,内部类中又有引用类型,这种方式显然不推荐使用。

        所以如果对于复杂对象的情况下,我们可以考虑使用序列化和反序列化的方式来进行克隆。序列化需要实现Serializable接口,它能做到真正的深拷贝。代码如下:

public class Person implements Serializable {

    private static final long serialVersionUID = 5093838173041282334L;
    private String            name;
    private int               age;
    private Phone             phone;

    public Person(String name, int age, Phone phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    public Person() {}

    private class Phone implements Serializable {

        private static final long serialVersionUID = 6683221508874474120L;
        private String            type;

        private Phone(String type) {
            this.type = type;
        }

        private String getType() {
            return type;
        }

        private void setType(String type) {
            this.type = type;
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Phone getPhone() {
        return phone;
    }

    public void setPhone(Phone phone) {
        this.phone = phone;
    }

    public Person clone(Person obj) {
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bout);
            oos.writeObject(obj);
            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bin);
            return (Person) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Person man = new Person("Robert Hou", 23, new Person().new Phone("iPhone 8 Plus"));
        Person cloneMan = man.clone(man);
        System.out.println("man: " + man);
        System.out.println("cloneMan: " + cloneMan);
        System.out.println("man == cloneMan: " + (man == cloneMan));
        System.out.println("man.getPhone() == cloneMan.getPhone(): " + (man.getPhone() == cloneMan.getPhone()));
        man.getPhone().setType("iPhone X");
        System.out.println("man.getPhone().getType(): " + man.getPhone().getType());
        System.out.println("cloneMan.getPhone().getType(): " + cloneMan.getPhone().getType());
    }
}

        需要说明的是:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义。这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放。运行结果如下:

man: com.hys.test.Person@42a57993
cloneMan: com.hys.test.Person@7cca494b
man == cloneMan: false
man.getPhone() == cloneMan.getPhone(): false
man.getPhone().getType(): iPhone X
cloneMan.getPhone().getType(): iPhone 8 Plus

猜你喜欢

转载自blog.csdn.net/weixin_30342639/article/details/80875030