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

简介: 在Java提供的API中,不需要手动创建抽象原型接口,因为Java已经内置了Cloneable抽象原型接口,自定义的类型只需实现该接口并重写Object.clone()方法即可完成本类的复制。通过查看JDK的源码可以发现,其实Cloneable是一个空接口。Java之所以提供Cloneable接口,只是为了在运行时通知Java虚拟机可以安全地在该类上使用clone()方法。而如果该类没有实现 Cloneable接口,则调用clone()方法会抛出 CloneNotSupportedException异常。一般情况下,如果使用clone()方法,则需满足以下条件。

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

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());
    }

运行程序,得到如下图所示的结果,与期望的结果一致。

20211102174433577.png

从运行结果来看,我们的确完成了深克隆。


【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦


本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!

如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。

相关文章
|
6月前
|
SQL Java 关系型数据库
MyBatis篇-分页
本文介绍了多种分页方式,包括自带rowbound内存分页、第三方插件pagehelper(通过修改SQL实现分页)、SQL分页(依赖limit或rownum等关键字)、数组分页(先查询全部数据再用subList分页)、拦截器分页(自定义拦截器为SQL添加分页语句)。最后总结了逻辑分页(内存分页,适合小数据量)和物理分页(直接在数据库层面分页,适合大数据量)的优缺点,强调物理分页优先于逻辑分页。
|
运维 Kubernetes Java
Java一分钟之Kubernetes(K8s):容器编排深入浅出
【6月更文挑战第6天】Kubernetes是容器编排的主流工具,简化分布式系统运维。本文介绍了K8s核心概念:节点、Pod、服务、部署和副本集。讨论了资源不足、依赖服务未就绪等常见问题及其解决策略,并提供创建Deployment的代码示例。学习K8s能提升部署效率和运维灵活性。
344 5
|
机器学习/深度学习 人工智能 算法
数据分享|R语言广义线性模型GLM:线性最小二乘、对数变换、泊松、二项式逻辑回归分析冰淇淋销售时间序列数据和模拟
数据分享|R语言广义线性模型GLM:线性最小二乘、对数变换、泊松、二项式逻辑回归分析冰淇淋销售时间序列数据和模拟
|
XML 编解码 前端开发
学习HTML笔记136
HTML5 SVG
115 0
|
2天前
|
人工智能 运维 安全
|
5天前
|
SpringCloudAlibaba 负载均衡 Dubbo
微服务架构下Feign和Dubbo的性能大比拼,到底鹿死谁手?
本文对比分析了SpringCloudAlibaba框架下Feign与Dubbo的服务调用性能及差异。Feign基于HTTP协议,使用简单,适合轻量级微服务架构;Dubbo采用RPC通信,性能更优,支持丰富的服务治理功能。通过实际测试,Dubbo在调用性能、负载均衡和服务发现方面表现更出色。两者各有适用场景,可根据项目需求灵活选择。
392 124
微服务架构下Feign和Dubbo的性能大比拼,到底鹿死谁手?
|
8天前
|
人工智能 JavaScript 测试技术
Qwen3-Coder入门教程|10分钟搞定安装配置
Qwen3-Coder 挑战赛简介:无论你是编程小白还是办公达人,都能通过本教程快速上手 Qwen-Code CLI,利用 AI 轻松实现代码编写、文档处理等任务。内容涵盖 API 配置、CLI 安装及多种实用案例,助你提升效率,体验智能编码的乐趣。
736 108
|
2天前
|
算法 Python
【轴承故障诊断】一种用于轴承故障诊断的稀疏贝叶斯学习(SBL),两种群稀疏学习算法来提取故障脉冲,第一种仅利用故障脉冲的群稀疏性,第二种则利用故障脉冲的额外周期性行为(Matlab代码实现)
【轴承故障诊断】一种用于轴承故障诊断的稀疏贝叶斯学习(SBL),两种群稀疏学习算法来提取故障脉冲,第一种仅利用故障脉冲的群稀疏性,第二种则利用故障脉冲的额外周期性行为(Matlab代码实现)
229 152