首页 > 编程笔记 > Java笔记

浅克隆和深克隆

我们在《Java原型设计模式》一节介绍了,在 Java 提供的 API 中,不需要手动创建抽象原型接口。Java 中的 Object 类提供了浅克隆的 clone() 方法。因为 Java 已经内置了 Cloneable 抽象原型接口,自定义的类型只需实现该接口并重写 Object.clone() 方法就可实现对象的浅克隆。

通过查看 JDK 的源码可以发现,Cloneable 是一个空接口,如下。
public interface Cloneable {
}
Java 之所以提供 Cloneable 接口,只是为了在运行时通知 Java 虚拟机可以安全地在该类上使用 clone() 方法。而如果该类没有实现 Cloneable 接口,那么调用 clone() 方法会抛出 CloneNotSupportedException 异常。

一般情况下,使用 clone() 方法需要满足以下条件。

我们在设计自定义类的 clone() 方法时,应当遵守这 3 个条件。一般来说,这 3 个条件中的前 2 个是必需的,第 3 个是可选的。

下面使用 Java 提供的 API 应用来实现原型模式,代码如下。
public class Client {
    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype type = new ConcretePrototype("original");
        System.out.println(type);
        //复制原型对象
        ConcretePrototype cloneType = null;
        try {
            cloneType = type.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        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() throws CloneNotSupportedException {
            ConcretePrototype cloneType = null;
            try {
                cloneType = (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return cloneType;
        }

        @Override
        public String toString() {
            return "ConcretePrototype{" + "desc='" + desc + '\'' + '}';
        }
    }
}
运行结果如下:

ConcretePrototype{desc='original'}
ConcretePrototype{desc='clone'}

super.clone() 方法直接从堆内存中以二进制流的方式进行复制,重新分配一个内存块,因此其效率很高。由于 super.clone() 方法基于内存复制,因此不会调用对象的构造函数,也就是不需要经历初始化过程。

在日常开发中,使用 super.clone() 方法并不能满足所有需求。如果类中存在引用对象属性,则原型对象与克隆对象的该属性会指向同一对象的引用。添加 ConcretePrototype 类实现 Cloneable 接口,代码如下。
@Data
public class ConcretePrototype implements Cloneable {
    private int age;
    private String name;
    private List<String> hobbies;

    @Override
    protected 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 class Client {
    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype type = new ConcretePrototype();
        type.setAge(18);
        type.setName("新宝库");
        List<String> hobbies = new ArrayList<String>();
        hobbies.add("书法");
        hobbies.add("美术");
        type.setHobbies(hobbies);

        //拷贝原型对象
        ConcretePrototype cloneType = type.clone();
        cloneType.getHobbies().add("技术控");

        System.out.println("原型对象:" + type);
        System.out.println("克隆对象:" + cloneType);
    }
}
运行结果如下:

原型对象:ConcretePrototype{age=18,name='新宝库',hobbies=[书法, 美术, 技术控]}
克隆对象:ConcretePrototype{age=18,name='新宝库',hobbies=[书法, 美术, 技术控]}

我们给克隆对象新增一个属性 hobbies(爱好)之后,发现原型对象也发生了变化,这显然不符合预期。因为我们希望克隆对象和原型对象是两个独立的对象,不再有联系。

从测试结果来看,应该是 hobbies 共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值,protoType 和 cloneType 的 hobbies 值都会改变。这就是我们常说的浅克隆,只是完整复制了值类型数据,没有复制引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?

Java 自带的 clone() 方法进行的就是浅克隆。如果我们想进行深克隆,可以直接在 super.clone() 后,手动给克隆对象的相关属性分配另一块内存,不过如果当原型对象维护很多引用属性的时候,手动分配会比较烦琐。因此,在 Java 中,如果想完成原型对象的深克隆,通常使用序列化(Serializable)的方式。

使用序列化实现深克隆

在上面的基础上继续改造,增加一个 deepClone() 方法。
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 deepCloneHobbies(){
        try {
            ConcretePrototype result = (ConcretePrototype)super.clone();
            result.hobbies = (List)((ArrayList)result.hobbies).clone();
            return result;
        } 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("新宝库");
    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());
}
运行结果如下,与期望的结果一致。

原型对象:ConcretePrototype{age=18, name='新宝库', hobbies=[书法, 美术]}
克隆对象:ConcretePrototype{age=18, name='新宝库', hobbies=[书法, 美术, 技术控]}
false
原型对象的爱好:[书法, 美术]
克隆对象的爱好:[书法, 美术, 技术控]
false

从以上结果来看,我们完成了深克隆。

所有教程

优秀文章