• Java 对实例进行深拷贝操作


    参考文章:https://blog.csdn.net/qq_39327985/article/details/100116148

    本人在项目中遇到过浅拷贝深拷贝的坑,使用一个对象以为是深拷贝,结果修改成员变量对象,导致两边内容均被修改。本文主要简介浅拷贝和深拷贝的概念以及三种实现深拷贝的方法。

    基本介绍

    Java 中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

    浅拷贝

    • 基本数据类型的成员变量:浅拷贝是直接进行值传递。
    • 引用数据类型的成员变量:即成员变量是某个数组,某个类对象等,此时浅拷贝是引用传递,是直接复制引用值(内存地址)给新的对象。因此新对象改变该成员变量值时,原来的对象成员变量值也会跟随改变,因为二者都是一个地址。
      浅拷贝是使用默认的 clone() 方法来实现(即不重写)

    对象浅拷贝实例

    比如一个bean如下:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class Company {
        private String id;
    
        private List<Employee> employees;
    
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public static class Employee{
            private String name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    直接赋值

    写demo进行测试:

    public static void main(String[] args) throws Exception {
        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        // 直接复制,company1和2相当于同一实例
        Company company2 = company1;
        System.out.println("company1 hashcode: " + company1.hashCode());
        System.out.println("company2 hashcode: " + company2.hashCode());
        System.out.println("company1 is: " + company1);
        System.out.println("company2 is: " + company2);
        System.out.println("company1 id hashcode: " + company1.getId().hashCode());
        System.out.println("company2 id hashcode: " + company2.getId().hashCode());
        company2.setId("2");
        company2.getEmployees().get(0).setName("two");
        System.out.println("change id hashcode: " + company1.getId().hashCode());
        System.out.println("change id hashcode: " + company2.getId().hashCode());
        System.out.println("change company1 is: " + company1);
        System.out.println("change company2 is: " + company2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出结果:

    company1 hashcode: 116644
    company2 hashcode: 116644
    company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    company2 is: Company(id=1, employees=[Company.Employee(name=one)])
    company1 id hashcode: 49
    company2 id hashcode: 49
    change id hashcode: 50
    change id hashcode: 50
    change company1 is: Company(id=2, employees=[Company.Employee(name=two)])
    change company2 is: Company(id=2, employees=[Company.Employee(name=two)])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    构造再赋值

    将直接赋值改为重新构造,本处使用了builder构造,实际上在Company类中写一个构造函数也是一样的效果,public Company(Company company) {this.id = company.getId();this.employees = company.getEmployees();}

    public static void main(String[] args) throws Exception {
        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        Company company2 = Company.builder().id(company1.getId()).employees(company1.getEmployees()).build();
        System.out.println("company1 hashcode: " + company1.hashCode());
        System.out.println("company2 hashcode: " + company2.hashCode());
        System.out.println("company1 is: " + company1);
        System.out.println("company2 is: " + company2);
        System.out.println("company1 id hashcode: " + company1.getId().hashCode());
        System.out.println("company2 id hashcode: " + company2.getId().hashCode());
        company2.setId("2");
        company2.getEmployees().get(0).setName("two");
        System.out.println("change id hashcode: " + company1.getId().hashCode());
        System.out.println("change id hashcode: " + company2.getId().hashCode());
        System.out.println("change company1 is: " + company1);
        System.out.println("change company2 is: " + company2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    输出结果:

    company1 hashcode: 116644
    company2 hashcode: 116644
    company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    company2 is: Company(id=1, employees=[Company.Employee(name=one)])
    company1 id hashcode: 49
    company2 id hashcode: 49
    change id hashcode: 49
    change id hashcode: 50
    change company1 is: Company(id=1, employees=[Company.Employee(name=two)])
    change company2 is: Company(id=2, employees=[Company.Employee(name=two)])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看出,id这种基本数据类型进行了值传递,但是employees数组是引用传递,改变company2数组中内容,company1和company2中都改变了。

    使用BeanUtils.copyProperties
        // org.springframework.beans.BeanUtils
        BeanUtils.copyProperties(company1, company2);
    
        // org.apache.commons.beanutils.BeanUtils
        BeanUtils.copyProperties(company2, company1);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:两个包的方法的参数是相反的。org.springframework.beans.BeanUtils中第一个参数是源,第二个参数是目标,org.apache.commons.beanutils.BeanUtils则相反

    输出的结果与上述相同,也是浅拷贝。

    数组浅拷贝

    从刚刚的实例看到,数组我们直接赋值,是浅拷贝,那么如下几种情况的数组处理,都是浅拷贝

      1. 遍历赋值
        代码如下
        List<Company.Employee> list1 = new ArrayList<>();
        company1.getEmployees().forEach(c -> list1.add(c));
    
    • 1
    • 2

    进行demo测试

        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        System.out.println("orignal list is: " + company1.getEmployees());
        List<Company.Employee> list1 = new ArrayList<>();
        company1.getEmployees().forEach(c -> list1.add(c));
        list1.get(0).setName("list1");
        System.out.println("list is: " + company1.getEmployees());
        System.out.println("list1 is: " + list1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
      1. addAll()方法
        代码如下
        List<Company.Employee> list1 = new ArrayList<>();
        list1.addAll(company1.getEmployees());
    
    • 1
    • 2

    进行demo测试

        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        System.out.println("orignal list is: " + company1.getEmployees());
        List<Company.Employee> list1 = new ArrayList<>();
        list1.addAll(company1.getEmployees());
        list1.get(0).setName("list1");
        System.out.println("list is: " + company1.getEmployees());
        System.out.println("list1 is: " + list1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
      1. 使用List构造方法
        代码如下
        List<Company.Employee> list1 = new ArrayList<>(company1.getEmployees());
    
    • 1

    进行demo测试

        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        System.out.println("orignal list is: " + company1.getEmployees());
        List<Company.Employee> list1 = new ArrayList<>(company1.getEmployees());
        list1.get(0).setName("list1");
        System.out.println("list is: " + company1.getEmployees());
        System.out.println("list1 is: " + list1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
      1. 使用System.arraycopy()方法
        实际上List的addAll方法底层,也是使用System.arraycopy()方法,如ArrayList对addAll()实现
        在这里插入图片描述

    代码如下

        System.arraycopy(temp, 0, list1, 0, temp.length);
    
    • 1

    进行demo测试

        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        System.out.println("orignal list is: " + company1.getEmployees());
        Object[] temp = company1.getEmployees().toArray();
        Company.Employee[] list1 = new Company.Employee[temp.length];
        System.arraycopy(temp, 0, list1, 0, temp.length);
        list1[0].setName("list1");
        System.out.println("list is: " + company1.getEmployees());
        System.out.println("list1 is: " + list1[0].getName());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    本组输出结果

    orignal list is: [Company.Employee(name=one)]
    list is: [Company.Employee(name=list1)]
    list1 is: list1
    
    • 1
    • 2
    • 3

    上述1 2 3 几组测试的结果均输出如下,即后者改变了,前者也跟着改变。

    orignal list is: [Company.Employee(name=one)]
    list is: [Company.Employee(name=list1)]
    list1 is: [Company.Employee(name=list1)]
    
    • 1
    • 2
    • 3

    深拷贝

    • 基本数据类型的成员变量:深拷贝是也进行值传递,基本数据类型都是值传递,所以一个对象修改该值,不影响另外一个对象的值。
    • 引用数据类型的成员变量:深拷贝会创建一个申请存储空间新的空间,然后把原来的内容(包括引用类型的数据)拷贝到新的地址。即新老对象完全是不同的地址。因此新对象改变该成员变量值时,原来的对象成员变量值是不会变动的。

    实现深拷贝的三种方式

    重写clone方法

    重写Company里clone

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class Company implements Cloneable{
        private String id;
    
        private List<Employee> employees;
    
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public static class Employee implements Cloneable{
            private String name;
    
            @Override
            public Employee clone() {
                //浅拷贝
                try {
                    // 直接调用父类的clone()方法
                    return (Employee) super.clone();
                } catch (CloneNotSupportedException e) {
                    return null;
                }
            }
        }
    
        /**
         *  重写clone()方法
         * @return
         */
        @Override
        public Company clone() {
            //浅拷贝
            try {
                Object cloneSuper = super.clone();
                Company res = (Company) cloneSuper;
                List<Employee> employeesCopy = Lists.newArrayList();
                this.employees.forEach(e ->{
                    employeesCopy.add((Employee) e.clone());
                });
                res.setEmployees(employeesCopy);
                return res;
            } catch (CloneNotSupportedException e) {
                return null;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    进行测试

    public static void main(String[] args) throws Exception {
        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
    
        // 调用重写的clone
        Company company2 = company1.clone();
    
        System.out.println("company1 hashcode: " + company1.hashCode());
        System.out.println("company2 hashcode: " + company2.hashCode());
        System.out.println("company1 is: " + company1);
        System.out.println("company2 is: " + company2);
        System.out.println("company1 id hashcode: " + company1.getId().hashCode());
        System.out.println("company2 id hashcode: " + company2.getId().hashCode());
    
        company2.setId("2");
        company2.getEmployees().get(0).setName("two");
        System.out.println("change id hashcode: " + company1.getId().hashCode());
        System.out.println("change id hashcode: " + company2.getId().hashCode());
        System.out.println("change company1 is: " + company1);
        System.out.println("change company2 is: " + company2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果

    company1 hashcode: 116644
    company2 hashcode: 116644
    company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    company2 is: Company(id=1, employees=[Company.Employee(name=one)])
    company1 id hashcode: 49
    company2 id hashcode: 49
    change id hashcode: 49
    change id hashcode: 50
    change company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    change company2 is: Company(id=2, employees=[Company.Employee(name=two)])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看出改变company2里数组的值,不会影响company1,已经实现了深拷贝

    注意
    effective java中13条提醒我们:谨慎地重写clone方法。

    考虑到与 Cloneable 接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。 虽然实现Cloneable接口对于final类没有什么危害,但应该将其视为性能优化的角度(会浪费复制),仅在极少数情况下才是合理的(详见第67条)。 通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用clone方法复制。

    因此不建议这样实现深拷贝。

    通过流进行序列号和反序列化

    需要将bean implements Serializable,如果有某个属性不需要序列化,可以将其声明为transient。

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class Company implements Serializable {
        private String id;
    
        private List<Employee> employees;
    
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public static class Employee implements Serializable {
            private String name;
        }
    
        //深度拷贝
        public Company deepCopy() throws Exception{
            // 序列化
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();
            // 反序列化
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (Company)ois.readObject();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    进行测试

    public static void main(String[] args) throws Exception {
        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        // 调用深拷贝方法
        Company company2 = company1.deepCopy();
        System.out.println("company1 hashcode: " + company1.hashCode());
        System.out.println("company2 hashcode: " + company2.hashCode());
        System.out.println("company1 is: " + company1);
        System.out.println("company2 is: " + company2);
        System.out.println("company1 id hashcode: " + company1.getId().hashCode());
        System.out.println("company2 id hashcode: " + company2.getId().hashCode());
        company2.setId("2");
        company2.getEmployees().get(0).setName("two");
        System.out.println("change id hashcode: " + company1.getId().hashCode());
        System.out.println("change id hashcode: " + company2.getId().hashCode());
        System.out.println("change company1 is: " + company1);
        System.out.println("change company2 is: " + company2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出结果

    company1 hashcode: 116644
    company2 hashcode: 116644
    company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    company2 is: Company(id=1, employees=[Company.Employee(name=one)])
    company1 id hashcode: 49
    company2 id hashcode: 49
    change id hashcode: 49
    change id hashcode: 50
    change company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    change company2 is: Company(id=2, employees=[Company.Employee(name=two)])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    使用Orika MapperFacade进行对象转换(推荐)

    本人比较推荐这种方法进行深拷贝。

    Orika MapperFacade中有两个核心的类,MapperFactory 、MapperFacade。

    • MapperFactory:可以用来注册字段的映射、转换器、自定义映射器、具体类型等等。
    • MapperFacade:它是实现映射过程的真正部分。有两种映射模式:
      1. 模式一:map(objectA, B.class)方法:将会生成一个新的实例B,然后把实例A中的属性赋值给实例B。方法有返回值,返回的是A的深拷贝后的实例B。
      2. 模式二:map(objectA, objectB)方法:A、B都是实例,把实例A的属性赋值到实例B中。无返回值。

    具体关于Orika MapperFacade的用法见Orika使用

    bean还原成最初的样子

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class Company {
        private String id;
    
        private List<Employee> employees;
    
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public static class Employee {
            private String name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    测试方法,写在controller层,因为mapperFacade需要注入,不能直接声明为static,因此没有使用main方法中测试

        List<Company.Employee> emps = Lists.newArrayList(Company.Employee.builder().name("one").build());
        Company company1 = Company.builder().id("1").employees(emps).build();
        // 使用mapper进行转换
        Company company2 = mapperFacade.map(company1, Company.class);
        System.out.println("company1 hashcode: " + company1.hashCode());
        System.out.println("company2 hashcode: " + company2.hashCode());
        System.out.println("company1 is: " + company1);
        System.out.println("company2 is: " + company2);
        System.out.println("company1 id hashcode: " + company1.getId().hashCode());
        System.out.println("company2 id hashcode: " + company2.getId().hashCode());
        company2.setId("2");
        company2.getEmployees().get(0).setName("two");
        System.out.println("change id hashcode: " + company1.getId().hashCode());
        System.out.println("change id hashcode: " + company2.getId().hashCode());
        System.out.println("change company1 is: " + company1);
        System.out.println("change company2 is: " + company2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    结果输出

    company1 hashcode: 116644
    company2 hashcode: 116644
    company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    company2 is: Company(id=1, employees=[Company.Employee(name=one)])
    company1 id hashcode: 49
    company2 id hashcode: 49
    change id hashcode: 49
    change id hashcode: 50
    change company1 is: Company(id=1, employees=[Company.Employee(name=one)])
    change company2 is: Company(id=2, employees=[Company.Employee(name=two)])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    BUUCTF SimpleRev
    若依+lodop+jasperreports+ireport 设计打印票据格式(一)
    本地JS文件批量压缩
    一比一还原axios源码(五)—— 拦截器
    计算机三级数据库高级查询
    Windows10下局域网的两台电脑间传输文件,设置文件夹共享
    鸿蒙开发(四)-低代码开发
    操作系统【OS】调度算法对比图
    【Ascend310】【Mindspore】安装mindspore后无法进行跑通样例
    国产开发板上打造开源ThingsBoard工业网关--基于米尔芯驰MYD-JD9X开发板
  • 原文地址:https://blog.csdn.net/weixin_43554422/article/details/127415376