前言
以前遇到过一个BUG,大概是这样的:
@Data
public class Company {
private String companyName;
private String companyAddr;
//公司员工
private List<Person> personList;
public Company(String companyName, String companyAddr, List<Person> personList) {
this.companyName = companyName;
this.companyAddr = companyAddr;
this.personList = personList;
}
@Data
public class Person {
private String name;
private int age;
private String addr;
public Person(String name, int age, String addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
public static void main(String args[]){
List<Person> personList = new ArrayList<>();
Person person1 = new Person("jack",23,"北京");
Person person2 = new Person("lisa",24,"上海");
personList.add(person1);
personList.add(person2);
//创建一个对象company1
Company company1 = new Company("xxx科技公司","北京",personList);
//创建一个对象company2,并把1赋值给2
Company company2 = company1;
//改变company1中的公司地址
company1.setCompanyAddr("深圳");
System.out.println(company1 == company2);
System.out.println("company1:"+company1.toString());
System.out.println("company2:"+company2.toString());
}
就像上面的代码那样,当我后面改变company1任意属性值时发现company2中的属性值也跟着改变了,这不是我想要的结果😵
原因分析:对象在使用“=”进行赋值时,采用的是引用传递(传递的是对象的一个引用),实际上两个对象指向的还是堆内存中的**同一个对象**,
所以当其中一个改变时,另一个也会跟着改变。
于是我想到了用浅拷贝
或者深拷贝
,但是实际上对这些概念还是很模糊,所以改完问题又去系统地学习了一下😏。
浅拷贝与深拷贝
上面的问题是使用深拷贝解决的,为什么不用浅拷贝呢?在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。在处理基本数据类型(如int,char,double)时,都是采用值传递,实际上是拷贝它的值。除此之外的其他类型都是按引用传递(传递的是对象的一个引用),实际上拷贝的知识对象的引用。对象在使用“=”赋值时也采用引用传递。
因为上面的对象company中还有一个属性personList,所以 使用浅拷贝 后,改变其他属性(基本数据类型)没有影响,但改变personList时另外一个的personList也会跟着改变。
浅拷贝
概念
浅拷贝就像上面说的一样,使用浅拷贝,拷贝前后的对象不是同一个对象。
①对于对象中基本数据类型的属性,因为是值传递的,所以直接将属性值赋值给新的对象。其中一个对象修改该值,不会影响另外一个。
②对于对象中的引用类型的属性,比如数组,集合或者类对象,因为是引用传递,所以浅拷贝只是把该属性的内存地址赋值给了新对象,它们指向了同一内存空间。改变其中一个对象中的该属性,另外一个就会受到影响。
浅拷贝的实现方法
要进行拷贝的类实现 Cloneable
接口,并覆写 clone()
方法。
@Data
public class Company implements Cloneable{
//公司姓名
private String companyName;
//公司地址
private String companyAddr;
//公司员工
private List<Person> personList;
public Company(String companyName, String companyAddr, List<Person> personList) {
this.companyName = companyName;
this.companyAddr = companyAddr;
this.personList = personList;
}
//重写clone()
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试方法:
public static void main(String args[]) throws CloneNotSupportedException {
List<Person> personList = new ArrayList<>();
Person person1 = new Person("jack",23,"北京");
Person person2 = new Person("lisa",24,"上海");
Person person3 = new Person("jone",23,"北京");
personList.add(person1);
personList.add(person2);
//创建一个对象company1
Company company1 = new Company("xxx科技公司","北京",personList);
//使用clone()获取对象company2
Company company2 = (Company) company1.clone();
//改变company2中的公司地址
company2.setCompanyAddr("深圳");
//改变company2中的personList
List<Person> list = company2.getPersonList();
list.add(person3);
System.out.println(company1 == company2);
System.out.println("company1:"+company1.toString());
System.out.println("company2:"+company2.toString());
}
结果:
可以看到两个对象是不相同的,但实际上两个对象中的personList还是指向一个地址(上面例子的代码中忘写了,可以试一下company1.getPersonList() == company2.getPersonList() 返回的是**true**
,改变基本类型属性值不影响,但改变company2的PersonList时,会影响到company1中的PersonList。
当然也有解决方法,那就是Person类也实现 Cloneable
接口,并覆写 clone()
方法,然后在Company
的clone()
方法中拿到拷贝后的对象,对拷贝后的对象的成员变量再调用clone()
方法。(这里已经相当于深拷贝了)。
@Data
public class Person implements Cloneable{
private String name;
private int age;
private String addr;
public Person(String name, int age, String addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
//复写clone
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
public class Company implements Cloneable{
//公司姓名
private String companyName;
//公司地址
private String companyAddr;
//公司员工
private List<Person> personList;
public Company(String companyName, String companyAddr, List<Person> personList) {
this.companyName = companyName;
this.companyAddr = companyAddr;
this.personList = personList;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Company company = (Company) super.clone();
List<Person> list = new ArrayList<>();
//依次重写personList中的对象Person 再set到company中,
//在调用company.clone()方法时也调用了person.clone();
if (!CollectionUtils.isEmpty(company.getPersonList())){
for (Person person : company.getPersonList()){
person = (Person) person.clone();
list.add(person);
}
company.setPersonList(list);
}
return company;
}
}
这里因为我的Company中的引用类型的变量是list,所以麻烦了一点,如果只是一个Person对象,就是这样:
private Person person;
@Override
protected Object clone() throws CloneNotSupportedException {
Company company = (Company) super.clone();
company.person = (Person)person.clone();
return company;
}
测试类和上面用的测试类一样,输出结果:
这样,两个对象就不会互相影响了。company1.getPersonList() == company2.getPersonList()
返回true。
深拷贝
概念
① 对于基本数据类型的成员变量,因为是值传递,所以是直接将属性值赋值给新的对象。其中一个对象修改该值,不会影响另外一个(这一点和浅拷贝是一样的)。
② 对于需要拷贝的对象中的引用类型的成员变量,比如集合,类对象等,深拷贝会新建一个对象空间,然后拷贝里面的内容,即它们也指向了不同的内存空间。改变其中一个,另一个不会受到影像(比如浅拷贝中的例子,如果使用深拷贝,)。
深拷贝的实现方法
三种深拷贝实现的方法:
① 第一种就是上面浅拷贝的那种解决方案,就是给每一层对象都要实现实现 Cloneable
接口,并覆写 clone()
方法,比较费劲,不推荐。
② 序列化与反序列化:使用org.apache.commons.lang.SerializationUtils的clone(Object obj)方法,拷贝的对象以及它的子对象都要实现Serializable接口。
Company company2 = (Company) SerializationUtils.clone(company1);
③ 把要深拷贝的对象从Object转成json,然后转回Object(推荐)
String json = JSON.toJSONString(company1);
Company company2 = JSON.parseObject(json, Company.class);
使用json的方式比序列化快一点。
具体代码就不贴了,测试方法和上面例子一样的😀。
总结
复制:创建了两个一摸一样的对象,地址相同,值也相同,改变其中一个对象的任一属性,另外一个也随之改变。
java的浅拷贝和深拷贝:区别就是在拷贝对象的时候,对引用类型
拷贝引用还是拷贝值。
如果拷贝前后两个对象地址不同,但只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,则是浅拷贝。
反之,拷贝前后两个对象地址不一样,对基本数据类型进行了拷贝,且在对该对象中的引用数据类型进行拷贝的时候,对该引用指向的对象也进行了拷贝,则是深拷贝。