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

61次阅读

共计 4289 个字符,预计需要花费 11 分钟才能阅读完成。

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

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 弹架构』可获取更多技术干货!

正文完
 0