关于java:一文读懂深克隆与浅克隆的关系

本文节选自《设计模式就该这样学》

1 剖析JDK浅克隆API带来的问题

在Java提供的API中,不须要手动创立形象原型接口,因为Java曾经内置了Cloneable形象原型接口,自定义的类型只需实现该接口并重写Object.clone()办法即可实现本类的复制。
通过查看JDK的源码能够发现,其实Cloneable是一个空接口。Java之所以提供Cloneable接口,只是为了在运行时告诉Java虚拟机能够平安地在该类上应用clone()办法。而如果该类没有实现 Cloneable接口,则调用clone()办法会抛出 CloneNotSupportedException异样。
个别状况下,如果应用clone()办法,则需满足以下条件。

(1)对任何对象o,都有o.clone() != o。换言之,克隆对象与原型对象不是同一个对象。

(2)对任何对象o,都有o.clone().getClass() == o.getClass()。换言之,复制对象与原对象的类型一样。

(3)如果对象o的equals()办法定义失当,则o.clone().equals(o)该当成立。

咱们在设计自定义类的clone()办法时,该当恪守这3个条件。一般来说,这3个条件中的前2个是必须的,第3个是可选的。
上面应用Java提供的API利用来实现原型模式,代码如下。


class Client {
    public static void main(String[] args) {
        //创立原型对象
        ConcretePrototype type = new ConcretePrototype("original");
        System.out.println(type);
        //复制原型对象
        ConcretePrototype cloneType = type.clone();
        cloneType.desc = "clone";
        System.out.println(cloneType);

    }
    static class ConcretePrototype implements Cloneable {
        private String desc;

        public ConcretePrototype(String desc) {
            this.desc = desc;
        }

        @Override
        protected ConcretePrototype clone() {
            ConcretePrototype cloneType = null;
            try {
                cloneType = (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return cloneType;
        }

        @Override
        public String toString() {
            return "ConcretePrototype{" +
                    "desc='" + desc + '\'' +
                    '}';
        }
    }
}

super.clone()办法间接从堆内存中以二进制流的形式进行复制,重新分配一个内存块,因而其效率很高。因为super.clone()办法基于内存复制,因而不会调用对象的构造函数,也就是不须要经验初始化过程。
在日常开发中,应用super.clone()办法并不能满足所有需要。如果类中存在援用对象属性,则原型对象与克隆对象的该属性会指向同一对象的援用。


@Data
public class ConcretePrototype implements Cloneable {

    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

批改客户端测试代码。


public static void main(String[] args) {
        //创立原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("书法");
        hobbies.add("美术");
        prototype.setHobbies(hobbies);
        System.out.println(prototype);
        //复制原型对象
        ConcretePrototype cloneType = prototype.clone();
        cloneType.getHobbies().add("技术控");

        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + cloneType);

    }
        

咱们给复制的对象新增一个属性hobbies(喜好)之后,发现原型对象也产生了变动,这显然不合乎预期。因为咱们心愿复制进去的对象应该和原型对象是两个独立的对象,不再有分割。从测试后果来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是援用的地址。这样的话,如果咱们批改任意一个对象中的属性值,protoType 和cloneType的hobbies值都会扭转。这就是咱们常说的浅克隆。只是残缺复制了值类型数据,没有赋值援用对象。换言之,所有的援用对象依然指向原来的对象,显然不是咱们想要的后果。那如何解决这个问题呢?
Java自带的clone()办法进行的就是浅克隆。而如果咱们想进行深克隆,能够间接在super.clone()后,手动给复制对象的相干属性调配另一块内存,不过如果当原型对象保护很多援用属性的时候,手动调配会比拟繁缛。因而,在Java中,如果想实现原型对象的深克隆,则通常应用序列化(Serializable)的形式。

2 应用序列化实现深克隆

在上节的根底上持续革新,减少一个deepClone()办法。


/**
 * Created by Tom.
 */
@Data
public class ConcretePrototype implements Cloneable,Serializable {

    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    public ConcretePrototype clone() {
        try {
            return (ConcretePrototype)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public ConcretePrototype deepClone(){
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            return (ConcretePrototype)ois.readObject();
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }


    }

    @Override
    public String toString() {
        return "ConcretePrototype{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", hobbies=" + hobbies +
                '}';
    }
}

客户端调用代码如下。


public static void main(String[] args) {
        //创立原型对象
        ConcretePrototype prototype = new ConcretePrototype();
        prototype.setAge(18);
        prototype.setName("Tom");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("书法");
        hobbies.add("美术");
        prototype.setHobbies(hobbies);

        //复制原型对象
        ConcretePrototype cloneType = prototype.deepCloneHobbies();
        cloneType.getHobbies().add("技术控");

        System.out.println("原型对象:" + prototype);
        System.out.println("克隆对象:" + cloneType);
        System.out.println(prototype == cloneType);


        System.out.println("原型对象的喜好:" + prototype.getHobbies());
        System.out.println("克隆对象的喜好:" + cloneType.getHobbies());
        System.out.println(prototype.getHobbies() == cloneType.getHobbies());

    }
        

运行程序,失去如下图所示的后果,与冀望的后果统一。

从运行后果来看,咱们确实实现了深克隆。

【举荐】Tom弹架构:珍藏本文,相当于珍藏一本“设计模式”的书

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我高兴!
如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源。关注微信公众号『 Tom弹架构 』可获取更多技术干货!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理