Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a; B.b=A.b;
在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用现有对象的部分或全部 数据。
Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。
一、浅拷贝(Shallow Copy)
1.1 浅拷贝的概念
①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
1.2 浅拷贝的实现方式
1.2.1 拷贝构造方法实现浅拷贝
public class CopyConstructor {
public static void main(String[] args) {
Age1 a1=new Age1(20);
Age2 a2=new Age2(40);
Person p1=new Person(a1,a2,"摇头耶稣");
Person p2=new Person(p1);
System.out.println("p1是"+p1);
System.out.println("p2是"+p2);
//修改p1的各属性值,观察p2的各属性值是否跟随变化
p1.setName("小傻瓜");
a1.setAge(99);
a2.setAge(99);
System.out.println("修改后的p1是"+p1);
System.out.println("修改后的p2是"+p2);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
//两个属性值:分别代表值传递和引用传递
//浅拷贝引用传递
private Age1 age1;
//再次重写构造方法
private Age2 age2;
//值传递
private String name;
public Person(Person person) {
this.age1 = person.getAge1();
this.age2 = new Age2(person.getAge2());
this.name = person.getName();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age1 {
private int age;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age2 {
private int age;
public Age2(Age2 age) {
this.age = age.getAge();
}
}
运行结果:
p1是Person(age1=Age1(age=20), age2=Age2(age=40), name=摇头耶稣)
p2是Person(age1=Age1(age=20), age2=Age2(age=40), name=摇头耶稣)
修改后的p1是Person(age1=Age1(age=99), age2=Age2(age=99), name=小傻瓜)
修改后的p2是Person(age1=Age1(age=99), age2=Age2(age=40), name=摇头耶稣)
**结果分析:**这里对Person类选择了两个具有代表性的属性值:一个是引用传递类型;另一个是字符串类型(属于常量)。
通过拷贝构造方法进行了浅拷贝,各属性值成功复制。其中,p1值传递部分的属性值发生变化时,p2不会随之改变;而引用传递部分属性值未再次重写构造方法情况下(a1)发生变化时,p2也随之改变。
要注意:如果在拷贝构造方法中,对引用数据类型变量逐一开辟新的内存空间,创建新的对象,也可以实现深拷贝。而对于一般的拷贝构造,则一定是浅拷贝。
1.2.2 重写clone()方法进行浅拷贝
Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。但是需要注意:
1、Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。
2、使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。
对于这两点,解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。
public class CopyClone {
public static void main(String[] args) throws CloneNotSupportedException {
Age1 a=new Age1(20);
Student stu1=new Student("摇头耶稣",a,175);
//通过调用重写后的clone方法进行浅拷贝
Student stu2=(Student)stu1.clone();
System.out.println(stu1);
System.out.println(stu2.toString());
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1);
System.out.println(stu2);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Cloneable{
private String name;
private Age1 aage;
private int length;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age1 {
private int age;
}
运行结果:
Student(name=摇头耶稣, aage=Age1(age=20), length=175)
Student(name=摇头耶稣, aage=Age1(age=20), length=175)
Student(name=大傻子, aage=Age1(age=99), length=216)
Student(name=摇头耶稣, aage=Age1(age=99), length=175)
分析结果可以验证:
基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;
引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。
String类型非常特殊,所以额外设置了一个字符串类型的成员变量来进行说明。首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!也就是说,当我将name属性从“摇头耶稣”改为“大傻子"后,并不是修改了这个数据的值,而是把这个数据的引用从指向”摇头耶稣“这个常量改为了指向”大傻子“这个常量。在这种情况下,另一个对象的name属性值仍然指向”摇头耶稣“不会受到影响。
二、深拷贝(Deep Copy)
2.1 深拷贝的概念
对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
2.2深拷贝的实现方式
2.2.1 构造函数
我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。
// 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setaAge(new Age(99));
2.2.2 重载clone()方法
通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。
public class CopyClone {
public static void main(String[] args) throws CloneNotSupportedException {
Age2 a2=new Age2(40);
Student2 stu1=new Student2("摇头耶稣",a2,175);
//通过调用重写后的clone方法进行浅拷贝
Student2 stu2=(Student2)stu1.clone();
System.out.println(stu1);
System.out.println(stu2.toString());
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a2.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1);
System.out.println(stu2);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student2 implements Cloneable{
private String name;
private Age2 aage2;
private int length;
@Override
public Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
//调用Age类的clone方法进行深拷贝
//先将obj转化为学生类实例
Student2 stu=(Student2)obj;
//学生类实例的Age对象属性,调用其clone方法进行拷贝
stu.aage2=(Age2)stu.getAage2().clone();
return obj;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age2 implements Cloneable {
private int age;
public Age2(Age2 age) {
this.age = age.getAge();
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
运行结果:
Student2(name=摇头耶稣, aage2=Age2(age=40), length=175)
Student2(name=摇头耶稣, aage2=Age2(age=40), length=175)
Student2(name=大傻子, aage2=Age2(age=99), length=216)
Student2(name=摇头耶稣, aage2=Age2(age=40), length=175)
2.2.3 Apache Commons Lang序列化
Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。
public class CopyApacheCommonsLang {
public static void main(String[] args) {
Age1 a1=new Age1(20);
Student1 stu1=new Student1("摇头耶稣",a1,175);
//通过调用重写后的clone方法进行浅拷贝
Student1 stu2= SerializationUtils.clone(stu1);
System.out.println(stu1);
System.out.println(stu2.toString());
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a1.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1);
System.out.println(stu2);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student1 implements Serializable {
private String name;
private Age1 aage1;
private int length;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age1 implements Serializable {
private int age;
}
2.2.4 Gson序列化
public class CopyGson {
public static void main(String[] args) {
Age1 a1=new Age1(20);
Student1 stu1=new Student1("摇头耶稣",a1,175);
// 使用Gson序列化进行深拷贝
Gson gson = new Gson();
Student1 stu2 = gson.fromJson(gson.toJson(stu1), Student1.class);
System.out.println(stu1);
System.out.println(stu2.toString());
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a1.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1);
System.out.println(stu2);
}
}
maven依赖
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
2.2.5 Jackson序列化
public class CopyJackson {
public static void main(String[] args) throws JsonProcessingException {
Age1 a1=new Age1(20);
Student1 stu1=new Student1("摇头耶稣",a1,175);
// 使用Jackson序列化进行深拷贝
ObjectMapper objectMapper = new ObjectMapper();
Student1 stu2 = objectMapper.readValue(objectMapper.writeValueAsString(stu1), Student1.class);
System.out.println(stu1);
System.out.println(stu2.toString());
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a1.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1);
System.out.println(stu2);
}
}
2.3 深拷贝几种方式的比较
深拷贝方法 | 优点 | 缺点 |
---|---|---|
构造函数 | 1. 底层实现简单 2. 不需要引入第三方包 3. 系统开销小 4. 对拷贝类没有要求,不需要实现额外接口和方法 | 1. 可用性差,每次新增成员变量都需要新增新的拷贝构造函数 |
重载clone()方法 | 1. 底层实现较简单 2. 不需要引入第三方包 3. 系统开销小 | 1. 可用性较差,每次新增成员变量可能需要修改clone()方法 2. 拷贝类(包括其成员变量)需要实现Cloneable接口 |
Apache Commons Lang序列化 | 1. 可用性强,新增成员变量不需要修改拷贝方法 | 1. 底层实现较复杂 2. 需要引入Apache Commons Lang第三方JAR包 3. 拷贝类(包括其成员变量)需要实现Serializable接口 4. 序列化与反序列化存在一定的系统开销 |
Gson序列化 | 1. 可用性强,新增成员变量不需要修改拷贝方法 2. 对拷贝类没有要求,不需要实现额外接口和方法 | 1. 底层实现复杂 2. 需要引入Gson第三方JAR包 3. 序列化与反序列化存在一定的系统开销 |
Jackson序列化 | 1. 可用性强,新增成员变量不需要修改拷贝方法 | 1. 底层实现复杂 2. 需要引入Jackson第三方JAR包 3. 拷贝类(包括其成员变量)需要实现默认的无参构造函数 4. 序列化与反序列化存在一定的系统开销 |