参考文章: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则相反
输出的结果与上述相同,也是浅拷贝。
数组浅拷贝
从刚刚的实例看到,数组我们直接赋值,是浅拷贝,那么如下几种情况的数组处理,都是浅拷贝
-
- 遍历赋值
代码如下
- 遍历赋值
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);
-
- addAll()方法
代码如下
- 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);
-
- 使用List构造方法
代码如下
- 使用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);
-
- 使用System.arraycopy()方法
实际上List的addAll方法底层,也是使用System.arraycopy()方法,如ArrayList对addAll()实现
- 使用System.arraycopy()方法
代码如下
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:它是实现映射过程的真正部分。有两种映射模式:
- 模式一:map(objectA, B.class)方法:将会生成一个新的实例B,然后把实例A中的属性赋值给实例B。方法有返回值,返回的是A的深拷贝后的实例B。
- 模式二: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)])