【Java基础】对象深克隆和浅克隆的原理及实现

为什么要对象克隆?

当需要把一个对象的值赋值给另一个对象时,使用new和赋值语句或者set注入都是可以的,但是,这会花费大量开销去做,效率低,并且还会产生冗余代码。克隆对象可以将对象直接复制,被复制的对象保留原对象的所有属性值

了解Cloneable接口和Serializable接口

Java语言提供的Cloneable接口和Serializable接口都是空接口,也称为标识接口标识接口中没有任何方法作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

实现对象克隆有两种方式:

  1. 实现Cloneable接口( 该接口为标记接口,不含任何方法)并重写Object类中的clone()方法

  2. 实现Serializable接口,通过对象序列化反序列化实现克隆,可以实现真正的深度克隆

浅克隆(ShallowClone)和深克隆(DeepClone)区别

浅克隆和深克隆的主要区别在于是否支持引用类型成员变量的复制

  1. 浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。

    也就是说在浅克隆中,如果原对象的成员变量是值类型,将复制一份给克隆对象;如果原对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原对象和克隆对象的引用类型成员变量指向相同的内存地址。
    在这里插入图片描述
    在Java中,通过重写clone()方法实现浅克隆。

  2. 深克隆:是在引用类型的类中也重写了clone(),是clone的嵌套,并且在clone方法中又对没有克隆的引用类型做差异化复制,克隆后的对象与原对象之间完全不会影响,但是属性值完全相同。使用clone实现的深克隆实际上是在浅克隆中嵌套了浅克隆

    在深克隆中,无论原对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原对象的所有引用对象也复制一份给克隆对象。也就是说,除了对象本身被复制外,对象所包含的所有引用/非引用成员变量也将复制。
    在这里插入图片描述
    在Java中,如果需要实现深克隆,可以通过重写clone()方法或序列化(Serialization)实现

  3. 序列化/反序列化:如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。通过序列化将对象写到IO流中,再从流里将其反序列化读出来,可以实现深克隆。 需要注意的是列化的对象必须实现Serializable接口,否则无法实现序列化操作。

    1. 对象序列化后写入流中,不存在引用的概念,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。

    2. 基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这种方案明显优于重写clone方法克隆对象。让问题在编译的时候暴露出来,而不是在运行时暴露出来

如何实现浅克隆
  1. 被复制的类需要实现Clonenable接口, 该接口为标记接口(不含任何方法)

  2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。

  3. 将得到的复制对象返回

    需要注意:Object的clone()方法是在java平台层实现的native方法,具有开销小,速度快的特点。而且,原始的Object方法是被protected修饰的,在这里需要修改为public,如果不这么做,浅克隆时没有问题,深克隆就会遇到权限不够的问题。 java继承还有个原则,就是子类覆写父类方法,访问修饰符权限不能低于父类。

例:创建Student对象

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor//生成所有参数的有参构造方法
@ToString
public class Student implements Cloneable{
    private int studentId;
    private String studentName;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

创建Teacher对象

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * @Description TODO 教师类
 */
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor //生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class Teacher implements Cloneable {
    private Integer id;
    private String name;
    private Double money;
    private Student student;
    
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    /**
     * 测试浅克隆
     *
     * @param args
     * @throws CloneNotSupportedException
     */
    public static void main(String[] args) throws CloneNotSupportedException {
        Student xiaoMing = new Student(2, "小明同学");
        Teacher zhangShan = new Teacher(365, "张三老师", 1000.25, xiaoMing);

        //克隆对象
        Teacher zhangShanClone = (Teacher) zhangShan.clone();

        System.out.println("zhangShan=>"+zhangShan);
        System.out.println("zhangShanClone=>"+zhangShanClone);
        System.out.println("-----------------------");
        
//1.打印内存地址发现不一致,说明调用clone()会创建一个新的对象
//com.demo.clone.Teacher@4fca772d
//com.demo.clone.Teacher@9807454

//2.注释掉Role类的@ToString()观察成员变量student,发现student内存地址指定同一位置
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)

//3.修改克隆对象的student属性值,然后放开Role的@ToString()注释观察成员变量student,发现克隆对象改变了student属性值,导致原对象属性值也改变了
        Student xiaoMingClone = zhangShanClone.getStudent();        //获取克隆对象student属性,并修改属性值
        xiaoMingClone.setStudentName("克隆小明同学");
        System.out.println("zhangShan=>"+zhangShan);
        System.out.println("zhangShanClone=>"+zhangShanClone);
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
    }
}

不注释Student类ToString()执行结果:
在这里插入图片描述
注释Student类ToString()执行结果:
在这里插入图片描述
可以看出浅克隆只复制基本类型的数据引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。

  1. 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
  2. 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。

通过上面的Teacher例子可以看到,浅拷贝会带来数据安全方面的隐患,例如我只是想修改了克隆对象 zhangShanClone成员变量student,但是原对象Teacher实例zhangShan成员变量studen也被修改了,因为它们都是指向的同一个地址。

如何实现深克隆

前面两步跟浅克隆差不多后面一步不同

  1. 被复制的类需要实现Clonenable接口, 该接口为标记接口(不含任何方法)
  2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。
  3. 将得到的复制对象返回,(深克隆做法)如果对象中有引用对象那么对引用对象再克隆一次。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
 * @Description TODO 用户
 * @Author JianPeng OuYang
 * @Date 2020/1/7 14:26
 * @Version v1.0
 */
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor //生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class User implements Cloneable {
    private String userName;
    private String password;
    private Role role;


    @Override
    public Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.setRole((Role) user.getRole().clone());//调用Role的clone
        return user;
    }



    /**
     * 深克隆
     * 是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。
     *
     * @param args
     * @throws CloneNotSupportedException
     */
    public static void main(String[] args) throws CloneNotSupportedException {
        Role role = new Role(1, "超级管理员");
        User zhangShan = new User("admin", "123456", role);

        //克隆用户张三
        User zhangShanClone = (User) zhangShan.clone();
        zhangShanClone.setUserName("test");

        //获取李四的角色类型,修改角色类型为普通用户
        Role zhangShanRole = zhangShanClone.getRole();
        zhangShanRole.setRoleId(3);
        zhangShanRole.setRoleName("普通用户");

        System.out.println(zhangShan);
        System.out.println(zhangShanClone);
        System.out.println("----------------------");
        //User对象克隆内存地址不同,说明clone后是会创建一个新的对象
        //com.demo.clone.User@4fca772d
        //com.demo.clone.User@9807454

    }
}

不注释Role类ToString()执行结果:
在这里插入图片描述
注释Role类ToString()执行结果:
在这里插入图片描述
可以看出深拷贝在拷贝时开辟了一个新的内存空间保存引用类型成员变量,从而实现真正内容上的拷贝。

  1. 对于基本数据类型的成员变量是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
  2. 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
  3. 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝
  4. 每次新增新引用类型成员变量都需要实现 Cloneable 并重写 clone() 方法
  5. 深拷贝相比于浅拷贝速度较慢并且花销较大。
如何通过对象序列化进行深克隆

如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦,需要每个对象都实现 Cloneable 并重写 clone() 方法,这时我们可以用序列化来解决多层克隆问题 ,将对象序列化,转换为二进制码,然后再反序列化成对象,最后赋值。从而实现克隆

  1. 在克隆类中自定义方法或者重写clone()方法,使用IO流实现序列化逻辑
    1. 序列化:通过对象输出流ObjectOutputStream将对象写入到字节数组输出流ByteArrayOutputStream
    2. 反序列化:读取字节数组到字节数组输入流ByteArrayInputStream中,然后使用对象输入流ObjectInputStream读取写入对象
  2. 所有相关的类都要实现Serializable接口,并定义serialVersionUID
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.*;
/**
 * @Description TODO 父亲类
 * @Author JianPeng OuYang
 * @Date 2020/1/8 10:42
 * @Version v1.0
 */
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor//生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class Father implements Serializable {
    private static final long serialVersionUID = -3442179150194293202L;

    private Integer fatherId;
    private String fatherName;
    private Son son;

    public static void main(String[] args) {
        Son son = new Son(11, "儿子");
        Father father = new Father(1, "爸爸", son);

        //克隆Father对象
        Father fatherClone = father.clone();
        //修改克隆对象成员变量的sonId
        Son sonClone = fatherClone.getSon();
        sonClone.setSonId(111111);

        System.out.println("father=>" + father);
        System.out.println("fatherClone=>" + fatherClone);
    }

    @Override
    public Father clone() {
        //用于序列化对象IO流
        //字节数组输出流
        ByteArrayOutputStream bos=null;
        //对象输出流
        ObjectOutputStream oos = null;

        //用于饭序列化对象IO流
        //字节数组输入流
        ByteArrayInputStream bis = null;
        //对象输入流
        ObjectInputStream ois = null;
        try  {
            //-------序列化对象
             bos = new ByteArrayOutputStream();
            //对象输出流=》将对象通过对象输出流写入字节数组输出流
             oos = new ObjectOutputStream(bos);
            //将当前对象写入对象流中
            oos.writeObject(this);

            //--------反序列化对象
            //读取字节输出流返回的字节数组
             bis = new ByteArrayInputStream(bos.toByteArray());
            //解析字节输入流写入的字节
             ois = new ObjectInputStream(bis);

            //-------读取对象
            Father father = (Father) ois.readObject();

            return father;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            close(bis,bos);
            close(ois,oos);
        }
        return null;
    }

    public void close(InputStream inputStream, OutputStream outputStream) {
        try {
            if (outputStream != null) {
                outputStream.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.*;
/**
 * @Description TODO 儿子类
 */
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor//生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class Son implements Serializable {
    private static final long serialVersionUID = -86552601995157672L;

    private Integer sonId;
    private String sonName;
}
Apache Commons Lang的API序列化

Java提供了序列化的能力,我们可以先将原对象进行序列化,再反序列化生成克隆对象。但是,使用序列化的前提是克隆的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。

导入依赖

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.1</version>
        </dependency>
/**
 * 用户
 */
public class User implements Serializable {
    private String name;
    private Address address;
    // constructors, getters and setters 省略
}
/**
 * 地址
 */
public class Address implements Serializable {
    private String city;
    private String country;
    // constructors, getters and setters 省略
}
@Test
public void serializableCopy() {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 使用Apache Commons Lang序列化进行深拷贝
    User copyUser = (User) SerializationUtils.clone(user);

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());
}

优点:

  1. 可用性强,新增成员变量不需要修改clone()方法

缺点:

  1. 底层实现复杂,需要引入Apache Commons Lang第三方JAR包
  2. 拷贝类(包括其成员变量)需要实现Serializable接口
  3. 序列化与反序列化存在一定的系统开销
Gson序列化
  1. Gson是谷歌官方推出的支持 JSON 和Java对象相互转换的 Java序列化/反序列化库
  2. Gson可以将对象序列化成JSON,也可以将JSON反序列化成对象,所以我们可以用它进行深拷贝。

导入依赖

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.3</version>
        </dependency>

测试用例

//沿用 Apache Commons Lang的API序列化 的 User类和Address类
@Test
public void gsonCopy() {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 使用Gson序列化进行深拷贝
    Gson gson = new Gson();
    User copyUser = gson.fromJson(gson.toJson(user), User.class);

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

优点:

  1. 可用性强,不需要实现任何接口或方法,也不需要修改clone()方法

缺点:

  1. 底层实现复杂,要导入Gson第三方Jar包
    2.序列化与反序列化存在一定的系统开现额外接口和方法销
Jackson序列化

Jackson与Gson相似,也是一种JSON 和Java对象相互转换的 Java序列化/反序列化库,明显不同的地方是拷贝的类(包括其成员变量)需要有默认的无参构造函数

导入依赖

<!-- Jackson依赖库 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <!--2.8.8-->
    <version>${dependency.version.jackson}</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${dependency.version.jackson}</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>${dependency.version.jackson}</version>
</dependency>
<!--jackson-core——核心包(必须),提供基于“流模式”解析的API。-->
<!--jackson-databind——数据绑定包(可选),提供基于“对象绑定”和“树模型”相关API。-->
<!--jackson-annotations——注解包(可选),提供注解功能。-->
/**
 * 地址
 */
public class Address {
    private String city;
    private String country;

    public Address() {}
    
    //getters and setters省略
}
/**
 * 用户
 */
public class User {
    private String name;
    private Address address;

    public User() {}

    //getters and setters省略
}

测试用例

@Test
public void jacksonCopy() throws IOException {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 使用Jackson序列化进行深拷贝
    ObjectMapper objectMapper = new ObjectMapper();
    User copyUser = objectMapper.readValue(objectMapper.writeValueAsString(user), User.class);

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

优点:

  1. 可用性强,不需要实现任何接口或方法,也不需要修改clone()方法

缺点:

  1. 底层实现复杂,要导入JackJson第三方Jar包
  2. 序列化与反序列化存在一定的系统开销
  3. 拷贝类(包括其成员变量)需要有默认无参构造方法
fastjson序列化

fastjson 是阿里巴巴的开源JSON解析库,JSON 和Java对象相互转换的 Java序列化/反序列化库

导入依赖

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
public class UserGroup {
    private String name;
    List<User> userList = new ArrayList<>();
     //getters and setters省略
}
public class User {
    private String name;
    private String age;
    
    public User(String name, String age) {
        super();
        this.name = name;
        this.age = age;
    }
    //getters and setters省略
}

测试案例

@Test
 public  void testFastJson(String[] args) {
        // 构建用户geust  
        User guestUser = new User("guest","18");  
        // 构建用户root  
        User rootUser = new User("root","28");  
        // 构建用户组对象  
        UserGroup group = new UserGroup();  
        group.setName("admin");  
        group.getUserList().add(guestUser);  
        group.getUserList().add(rootUser);  
        
        // 用户组对象转JSON串
        String jsonString = JSON.toJSONString(group);
        System.out.println("jsonString:" + jsonString);  
        
        // JSON串转用户组对象  
        UserGroup group2 = JSON.parseObject(jsonString, UserGroup.class);
        System.out.println("group2:" + group2);  
        
        // 构建用户对象数组  
        User[] users = new User[2];  
        users[0] = guestUser;  
        users[1] = rootUser;  
        // 用户对象数组转JSON串  
        String jsonString2 = JSON.toJSONString(users);  
        System.out.println("jsonString2:" + jsonString2);  
        // JSON串转用户对象列表  
        List<User> users2 = JSON.parseArray(jsonString2, User.class);  
        System.out.println("users2:" + users2);  
        
        
    }
总结

具体用那种克隆方式因实际情况而定,每种方法都有他的优缺点,适用情况也不同.

  1. 如果实际场景中需要克隆的对象都是基本数据类型就使用浅克隆即可,
  2. 如果含有单个引用对象(即类中只要一层引用关系)如上面的数组之类的,那么简单的深克隆即可,
  3. 如果多层克隆则使用序列化方式, 但是使用序列化的话相比较前面两种是比较消耗内存的。

克隆实现方式 深克隆与浅克隆
Java对象克隆——浅克隆和深克隆的区别
.

发布了62 篇原创文章 · 获赞 109 · 访问量 5281

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/103858729