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;
    }
}
直接赋值

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

输出结果:

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)])
构造再赋值

将直接赋值改为重新构造,本处使用了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);
}

输出结果:

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)])

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

使用BeanUtils.copyProperties
    // org.springframework.beans.BeanUtils
    BeanUtils.copyProperties(company1, company2);

    // org.apache.commons.beanutils.BeanUtils
    BeanUtils.copyProperties(company2, company1);

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

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

数组浅拷贝

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

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

进行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. addAll()方法
      代码如下
    List<Company.Employee> list1 = new ArrayList<>();
    list1.addAll(company1.getEmployees());

进行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. 使用List构造方法
      代码如下
    List<Company.Employee> list1 = new ArrayList<>(company1.getEmployees());

进行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. 使用System.arraycopy()方法
      实际上List的addAll方法底层,也是使用System.arraycopy()方法,如ArrayList对addAll()实现
      在这里插入图片描述

代码如下

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

进行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());

本组输出结果

orignal list is: [Company.Employee(name=one)]
list is: [Company.Employee(name=list1)]
list1 is: list1

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

orignal list is: [Company.Employee(name=one)]
list is: [Company.Employee(name=list1)]
list1 is: [Company.Employee(name=list1)]

深拷贝

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

实现深拷贝的三种方式

重写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;
        }
    }
}

进行测试

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

输出结果

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)])

可以看出改变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();
    }
}

进行测试

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

输出结果

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)])

使用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;
    }
}

测试方法,写在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);

结果输出

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)])
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值