深入理解 Java 中的对象克隆

大家好,我是磊磊落落,目前我在技术上主要关注:Java、Golang、架构设计、云原生和自动化测试。欢迎来我的博客(leileiluoluo.com)获取我的最近更新!

在 Java 中,对象克隆指的是创建一个现有对象的副本。该副本具有与原始对象相同的状态和属性,但在内存中两者是独立存在的,针对其中一个对象的修改不会影响到另一个对象。

要使一个类能够被克隆,需要满足以下条件:

  • 实现 Cloneable 接口

    Cloneable 是一个标记接口,没有任何方法,实现了该接口,即表示该类可以被克隆。

    Cloneable 接口的定义如下:

    java 复制代码
    package java.lang;
    
    public interface Cloneable {}
  • 重写 clone() 方法

    重写 Object 类中定义的受保护 clone() 方法,并将其访问修饰符设置为 public。而且按照约定,需要使用 super.clone() 调用 Objectclone() 方法来实现逐字段拷贝。

    clone() 方法在 Object 类中的定义如下:

    java 复制代码
    package java.lang;
    
    public class Object {
    
        @IntrinsicCandidate
        protected native Object clone() throws CloneNotSupportedException;
    }

    若不实现 Cloneable 接口,则调用 super.clone() 时会抛出 CloneNotSupportedException

注意:Java 中针对对象克隆的这一设计存在一定的「缺陷」。一个类支持克隆需要实现 Cloneable 接口,但 clone() 方法却没定义在该接口中。所以,即便一个类在声明上实现了该接口,但无法强制它必须含有 clone() 方法。

下面即尝试使用一下对象克隆。

1 尝试使用 clone() 方法

下面尝试新建一个房子(House)类,里边有名称(name)、大小(size)和冰箱(refrigerator)三个属性。该类实现了 Cloneable 接口并重写了 Objectclone() 方法。

java 复制代码
public class House implements Cloneable {
    private String name;
    private Integer size;
    private Refrigerator refrigerator;

    public House(String name, Integer size, Refrigerator refrigerator) {
        this.name = name;
        this.size = size;
        this.refrigerator = refrigerator;
    }

    @Override
    public House clone() {
        try {
            return (House) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    public static class Refrigerator {
        private String name;

        public Refrigerator(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) {
        House house1 = new House("Larry's House", 100, new Refrigerator("Larry's Refrigerator"));

        House house2 = house1.clone();
        house2.name = "Jacky's House";
        house2.size = 99;
        house2.refrigerator.name = "Jacky's Refrigerator";

        System.out.println(house1); // House@404b9385
        System.out.println(house1.name); // Larry's House
        System.out.println(house1.size); // 100
        System.out.println(house1.refrigerator); // House$Refrigerator@6d311334
        System.out.println(house1.refrigerator.name); // Jacky's Refrigerator

        System.out.println(house2); // House@682a0b20
        System.out.println(house2.name); // Jacky's House
        System.out.println(house2.size); // 99
        System.out.println(house2.refrigerator); // House$Refrigerator@6d311334
        System.out.println(house2.refrigerator.name); // Jacky's Refrigerator
    }
}

可以看到,House 类重写 clone() 方法时,按照约定直接调用了 super.clone() 来实现。

House 类的 main() 方法进行测试时发现:针对原始对象 house1,使用 house1.clone() 获取到了其克隆对象 house2。直接打印 house1house2,发现 hashCode 不同,说明两者是不同的实例,但两者的各属性值均相同。接着,house2namesizerefrigerator.name 重新赋值后,发现前两个字段的改变不会影响到 house1,但 refrigerator.name 的改变却影响到了 house1

这是为什么呢?

1.1 浅拷贝

这是因为,调用 super.clone() 获取一个对象的克隆时默认进行的是「浅拷贝」。即其只是新建了一个新的实例,然后参考原始对象对克隆对象进行逐个字段赋值。所以,字段若是原始类型或是指向不可变对象的引用类型,进行的是值传递,该字段赋值后即和原来的字段没有任何关系了;若字段是指向可变对象的引用类型,进行的是引用传递,该字段赋值后指向的其实还是原来字段指向的对象。

针对如上示例代码,house1house2 指向的两个对象在内存中的示意图如下:

1.2 深拷贝

可以看到,调用 super.clone() 仅实现了「浅拷贝」,如果我们想将指向的可变对象也重新复制一份,就需要额外做一些处理了。

如下代码在原来的基础上,将 Refrigerator 类也实现了 Cloneable 接口并重写了 clone() 方法。此外,还对 House 类的 clone() 方法做一点额外的处理(house.refrigerator = house.refrigerator.clone();):

java 复制代码
public class House implements Cloneable {
    private String name;
    private Integer size;
    private Refrigerator refrigerator;

    public House(String name, Integer size, Refrigerator refrigerator) {
        this.name = name;
        this.size = size;
        this.refrigerator = refrigerator;
    }

    @Override
    public House clone() {
        try {
            House house = (House) super.clone();
            house.refrigerator = house.refrigerator.clone();
            return house;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    public static class Refrigerator implements Cloneable {
        private String name;

        public Refrigerator(String name) {
            this.name = name;
        }

        @Override
        public Refrigerator clone() {
            try {
                return (Refrigerator) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        House house1 = new House("Larry's House", 100, new Refrigerator("Larry's Refrigerator"));

        House house2 = house1.clone();
        house2.name = "Jacky's House";
        house2.size = 99;
        house2.refrigerator.name = "Jacky's Refrigerator";

        System.out.println(house1); // House@404b9385
        System.out.println(house1.name); // Larry's House
        System.out.println(house1.size); // 100
        System.out.println(house1.refrigerator); // House$Refrigerator@6d311334
        System.out.println(house1.refrigerator.name); // Larry's Refrigerator

        System.out.println(house2); // House@682a0b20
        System.out.println(house2.name); // Jacky's House
        System.out.println(house2.size); // 99
        System.out.println(house2.refrigerator); // House$Refrigerator@3d075dc0
        System.out.println(house2.refrigerator.name); // Jacky's Refrigerator
    }
}

这样,house1 的克隆 house2refrigerator.name 重新赋值后即不会影响到 house1 了,这样即实现了深拷贝。

这时,house1house2 指向的两个对象在内存中的示意图如下:

但如果在冰箱类 Refrigerator 中新加一个苹果类(Apple)呢?即会出现与之前一样的问题。上面的代码只能实现到 Refrigerator 层的拷贝,而对于 Apple 又会是共享同一个对象。这样就需要我们重复如上的处理了(将 Apple 类也实现 Cloneable 接口并重写 clone() 方法,并改写 Refrigerator 类的 clone() 方法)。

总结一下,使用原生克隆方式需要遵循一定的规则,并且对于对象嵌套的情形处理起来还有点繁琐。

2 其它实现方式

原生的方式用起来比较麻烦?有没有其它的方式来实现对象克隆呢?

2.1 使用框架工具类

Spring 框架自带的 BeanUtils 工具类可以帮助我们实现一个对象的逐字段拷贝。使用该工具类时,对应的类无需实现 Cloneable 接口,也无需重写 clone() 方法。

下面即是 BeanUtils 工具类提供的可以实现 sourcetarget 拷贝的方法:

java 复制代码
BeanUtils.copyProperties(Object source, Object target);

使用时,需要添加如下 Maven 依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>6.1.5</version>
</dependency>

使用 BeanUtils.copyProperties() 实现 House 对象拷贝的示例代码如下:

java 复制代码
import org.springframework.beans.BeanUtils;

public class CopyableHouse {
    private String name;
    private Integer size;
    private Refrigerator refrigerator;

    public CopyableHouse() {
    }

    public CopyableHouse(String name, Integer size, Refrigerator refrigerator) {
        this.name = name;
        this.size = size;
        this.refrigerator = refrigerator;
    }

    public static class Refrigerator {
        private String name;

        public Refrigerator() {
        }

        public Refrigerator(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) {
        Refrigerator refrigerator = new Refrigerator("Larry's Refrigerator");
        CopyableHouse house1 = new CopyableHouse("Larry's House", 100, refrigerator);

        CopyableHouse house2 = new CopyableHouse();
        house2.refrigerator = new Refrigerator();
        BeanUtils.copyProperties(house1, house2);

        house2.name = "Jacky's House";
        house2.size = 99;
        house2.refrigerator.name = "Jacky's Refrigerator";

        System.out.println(house1); // CopyableHouse@75828a0f
        System.out.println(house1.name); // Larry's House
        System.out.println(house1.size); // 100
        System.out.println(house1.refrigerator); // CopyableHouse$Refrigerator@3abfe836
        System.out.println(house1.refrigerator.name); // Larry's Refrigerator

        System.out.println(house2); // CopyableHouse@2ff5659e
        System.out.println(house2.name); // Jacky's House
        System.out.println(house2.size); // 99
        System.out.println(house2.refrigerator); // CopyableHouse$Refrigerator@77afea7d
        System.out.println(house2.refrigerator.name); // Jacky's Refrigerator
    }
}

可以看到,使用 BeanUtils.copyProperties() 可以实现我们期望的效果。

2.2 使用拷贝构造器

另一种是我们提供一个拷贝构造器或一个静态工厂拷贝方法来自己实现对象的拷贝逻辑。

java 复制代码
public class House {
    private String name;
    private Integer size;
    private Refrigerator refrigerator;

    public House(House house) {
      // ...
    }

    public static House newInstance(House house) {
      // ...
    }
}
java 复制代码
House house2 = new House(house1);
// House house2 = House.newInstance(house1);

2.3 使用序列化与反序列化

还有一种方式是使用序列化与反序列化来实现对象的拷贝。即先将一个对象序列化到一个二进制文件,然后再将该对象反序列化出来,这样即是两个完全不同的实例。但要支持序列化,对应的类需要实现 Serializable 接口。此外,因为使用序列化与反序列化比较重,其性能不如原生的 clone() 方式。

下面使用 commons-lang3 中的 SerializationUtils 工具类来实现对象的克隆。

java 复制代码
SerializationUtils.clone(T object);

其 Maven 依赖如下:

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>

使用 SerializationUtils.clone() 实现 House 对象拷贝的示例代码如下:

java 复制代码
import org.apache.commons.lang3.SerializationUtils;

import java.io.Serial;
import java.io.Serializable;

public class SerializableHouse implements Serializable {
    @Serial
    private static final long serialVersionUID = -3606554850313928707L;

    private String name;
    private Integer size;
    private Refrigerator refrigerator;

    public SerializableHouse(String name, Integer size, Refrigerator refrigerator) {
        this.name = name;
        this.size = size;
        this.refrigerator = refrigerator;
    }

    public static class Refrigerator implements Serializable {
        @Serial
        private static final long serialVersionUID = 7744295794434285806L;

        private String name;

        public Refrigerator(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) {
        Refrigerator refrigerator = new Refrigerator("Larry's Refrigerator");
        SerializableHouse house1 = new SerializableHouse("Larry's House", 100, refrigerator);

        SerializableHouse house2 = SerializationUtils.clone(house1);
        house2.name = "Jacky's House";
        house2.size = 99;
        house2.refrigerator.name = "Jacky's Refrigerator";

        System.out.println(house1); // SerializableHouse@5e9f23b4
        System.out.println(house1.name); // Larry's House
        System.out.println(house1.size); // 100
        System.out.println(house1.refrigerator); // SerializableHouse$Refrigerator@7e6cbb7a
        System.out.println(house1.refrigerator.name); // Larry's Refrigerator

        System.out.println(house2); // SerializableHouse@5b37e0d2
        System.out.println(house2.name); // Jacky's House
        System.out.println(house2.size); // 99
        System.out.println(house2.refrigerator); // SerializableHouse$Refrigerator@4459eb14
        System.out.println(house2.refrigerator.name); // Jacky's Refrigerator
    }
}

可以看到,使用 SerializationUtils.clone() 克隆出的对象是一个与原始对象字段值完全相同但字段地址不同的新对象,对其中的字段重新赋值也不会对原始对象造成影响,符合我们的期望。

综上,本文介绍了 Java 中对象克隆的相关知识,包括对象克隆的概念、对象克隆的实现方式、浅拷贝与深拷贝、拷贝构造器等。此外还列出了一些适用的工具类来更便捷的帮助我们实现对象克隆。本文用于演示的所有完整代码已提交至本人 GitHub,欢迎关注或 Fork。

参考资料

[1] Effective Java (3rd Edition): Override clone judiciously - www.oreilly.com/library/vie...

[2] Wikipedia: clone (Java method) - en.wikipedia.org/wiki/Clone_...

[3] Java Platform SE 8: Interface Cloneable - docs.oracle.com/javase/8/do...

[4] Java Platform SE 8: Object.clone() - docs.oracle.com/javase/8/do...

[5] CSDN 博客:详解 Java 中的 clone 方法(原型模式)- blog.csdn.net/zhangjg_blo...

[6] SegmentFault:Java 浅克隆和深克隆 - segmentfault.com/a/119000002...

[7] Programming Guide: Java Clone and Cloneable - programming.guide/java/clone-...

[8] HowToDoInJava: Java Cloning, Deep and Shallow Copy, Copy Constructors - howtodoinjava.com/java/clonin...

[9] DigitalOcean: Java Object clone() Method - www.digitalocean.com/community/t...

[10] CSDN 博客:Java 实现对象克隆的三种方式(Cloneable 接口、Java 自身序列化、FastJson 序列化)- blog.csdn.net/dl962454/ar...

相关推荐
StayInLove8 分钟前
G1垃圾回收器日志详解
java·开发语言
对许12 分钟前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
无尽的大道16 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力25 分钟前
Java类和对象(下篇)
java
binishuaio29 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE31 分钟前
【Java SE】StringBuffer
java·开发语言
老友@31 分钟前
aspose如何获取PPT放映页“切换”的“持续时间”值
java·powerpoint·aspose
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
Upaaui1 小时前
Aop+自定义注解实现数据字典映射
java
zzzgd8161 小时前
easyexcel实现自定义的策略类, 最后追加错误提示列, 自适应列宽,自动合并重复单元格, 美化表头
java·excel·表格·easyexcel·导入导出