java对象拷贝
java对象拷贝
1、介绍
Java编写代码中,对象的拷贝是一个常见的操作。根据拷贝的层次和方式不同,可以分为深拷贝、浅拷贝和零拷贝
2、深拷贝
深拷贝是一种创建对象副本的方法,其中新对象与原始对象完全独立。这就意味着新对象的所有字段都被复制,并且如果字段是引用类型,那么递归地执行深拷贝,以确保新对象和原始对象不共享任何内部对象。
1、手动new实现:
通过创建一个新的对象,并逐个复制字段的值。如果字段是引用类型,需要递归地创建该字段的新实例,只不过这个过程比较繁琐。
2、使用序列化的方式
将对象序列化为字节流,然后再反序列化回一个新对象。这种方法要求对象及其所有组成部分都是可序列化的。
1、Apache Commons Lang序列化
可用性强,新增成员变量不需要修改拷贝方法
底层实现较复杂
需要引入Apache Commons Lang第三方JAR包
拷贝类(包括其成员变量)需要实现Serializable接口
序列化与反序列化存在一定的系统开销,Java 序列化: 是 Java 提供的一种内建机制,使用对象的序列化和反序列化机制来创建对象的深拷贝。这种方法依赖于 Java 的 ObjectOutputStream
和 ObjectInputStream
,并通过二进制数据进行存储。
- 序列化:
SerializationUtils.clone()
首先将对象序列化到一个ByteArrayOutputStream
中。这个过程将对象的状态转换为字节流。 - 反序列化: 然后,它从
ByteArrayInputStream
中读取字节流,并通过反序列化过程将其转换回对象。这些字节流中的数据完全描述了对象的状态,包括它所引用的所有对象。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3:3.5</version>
</dependency>
@Data
public class User implements Serializable {
private String name;
private Integer age;
}
调用SerializationUtils工具类,实现深拷贝(注意:SerializationUtils不能直接拷贝List类型)
user1的改变不会导致user2的改变,从而实现深拷贝
package com.shuizhu.study2;
import org.apache.commons.lang3.SerializationUtils;
//Apache Commons Lang序列化实现对象的深拷贝
public class Study01 {
public static void main(String[] args) {
User user1 = new User();
user1.setName("张三");
user1.setAge(18);
User user2 = SerializationUtils.clone(user1);
System.out.println("user1未改变前,user2的名字为:" + user2.getName());
user1.setName("李四");
System.out.println("user1改变后,user2的名字为:" + user2.getName());
}
}
List类型深拷贝
package com.shuizhu.study2;
import java.io.Serializable;
import java.util.List;
/**
* 用于深拷贝时,不需要去遍历List<User>集合,只需要拷贝UserCopyDTO 对象就可以
* 获取到新的List<User>集合
*/
@Data
public class UserCopyDTO implements Serializable {//必须实现Serializable接口
private List<User> users;
}
结果:list1改变后拷贝的list2不会随之改变
import org.apache.commons.lang3.SerializationUtils;
import java.util.ArrayList;
import java.util.List;
//Apache Commons Lang序列化实现List的深拷贝
public class Study02 {
public static void main(String[] args) {
List<User> list1 = new ArrayList<>();
User user1 = new User();
user1.setName("张三");
user1.setAge(18);
User user2 = new User();
user2.setName("李四");
user2.setAge(19);
list1.add(user1);
list1.add(user2);
//使用UserCopyDTO对象,专门用于拷贝List<User>类型数据,不需要再去遍历list1
UserCopyDTO userCopyDTO = new UserCopyDTO();
userCopyDTO.setUsers(list1);
//通过Apache Commons Lang序列化方式,把list01拷贝给list02
UserCopyDTO clone = SerializationUtils.clone(userCopyDTO);
List<User> list2 = clone.getUsers();
System.out.println("list1未改变前,list2的结果为:" + list2);
//改变list1集合中的user1对象
System.out.println("--------------------------------------------");
user1.setName("老六");
user1.setAge(78);
System.out.println("list1改变后,list2的结果为:" + list2);
}
}
2、Gson序列化
可用性强,新增成员变量不需要修改拷贝方法 ,对拷贝类没有要求,不需要实现额外接口和方法
底层实现复杂
需要引入Gson第三方JAR包
序列化与反序列化存在一定的系统开销
- JSON 序列化: 将对象转换成 JSON 字符串,通常需要额外的库(如 Jackson 或 Gson)来进行处理。JSON 主要用于数据交换和持久化,特别是在 Web 开发和 API 中。
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
创建Gson对象,使用该对象进行深拷贝(实体类不再需要实现Serializable接口)
只演示对象的深拷贝,LIst类型的深拷贝与之前的流程是相似的
package com.shuizhu.study3;
import com.google.gson.Gson;
//Gson序列化实现对象的深拷贝
public class Study01 {
public static void main(String[] args) {
User user1 = new User();
user1.setName("张三");
user1.setAge(18);
Gson gson = new Gson();
User user2 = gson.fromJson(gson.toJson(user1), User.class);
System.out.println("user1未改变前,user2的名字为:" + user2.getName());
user1.setName("李四");
System.out.println("user1改变后,user2的名字为:" + user2.getName());
}
}
3、Jackson序列化
可用性强,新增成员变量不需要修改拷贝方法
底层实现复杂
需要引入Jackson第三方JAR包
拷贝类(包括其成员变量)需要实现默认的无参构造函数
序列化与反序列化存在一定的系统开销
- JSON 序列化: 将对象转换成 JSON 字符串,通常需要额外的库(如 Jackson 或 Gson)来进行处理。JSON 主要用于数据交换和持久化,特别是在 Web 开发和 API 中。
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>core</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>databind</artifactId>
<version>2.2.2</version>
</dependency>
创建ObjectMapper对象,进行深拷贝(用法与Gson一致)
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
//Jackson序列化实现对象的深拷贝
public class Study01 {
public static void main(String[] args) {
User user1 = new User();
user1.setName("张三");
user1.setAge(18);
ObjectMapper mapper = new ObjectMapper();
User user2 = null;
try {
user2 = mapper.readValue(mapper.writeValueAsString(user1), User.class);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("user1未改变前,user2的名字为:" + user2.getName());
user1.setName("李四");
System.out.println("user1改变后,user2的名字为:" + user2.getName());
}
}
3、使用克隆clone接口
实现Cloneable接口并重写clone()方法。但这种方法有争议,因为它可能不提供真正的深拷贝,除非所有相关的类都正确实现了clone()方法。 对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝
@Data
public class User implements Cloneable{
private String name;
private Integer age;
@Override
protected User clone() throws CloneNotSupportedException {
return (User) super.clone();
}
}
结果:当user1改变后,user2的值不会改变
//Java深拷贝案列
public class Study03 {
public static void main(String[] args) throws CloneNotSupportedException {
User user1 = new User();
user1.setName("张三");
user1.setAge(18);
User user2 = user1.clone();
System.out.println("user1未改变前,user2的名字为:" + user2.getName());
user1.setName("李四");
System.out.println("user1未改变前,user2的名字为:" + user2.getName());
}
}
list的深拷贝: 结果:list1中的每个对象通过clone()添加list2中,当list1中的对象改变时,list2不会改变
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
//Java深拷贝案列
public class Study04 {
public static void main(String[] args) {
List<User> list1 = new ArrayList<>();
User user1 = new User();
user1.setName("张三");
user1.setAge(18);
User user2 = new User();
user2.setName("李四");
user2.setAge(19);
list1.add(user1);
list1.add(user2);
/
//通过clone方式,把list01拷贝给list02
List<User> list2 = new ArrayList<>();
//TODO 当数据量多时,建议使用对象的方式,把List当做属性,然后拷贝哦到一个新的对象中,从而不需要循环,可以见Apache Commons Lang序列化深拷贝方式
list1.forEach(user->{
try {
list2.add(user.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
});
System.out.println("list1未改变前,list2的结果为:" + list2);
//改变list1集合中的user1对象
System.out.println("--------------------------------------------");
user1.setName("老六");
user1.setAge(78);
System.out.println("list1改变后,list2的结果为:" + list2);
}
}
4、使用java反射Bean的get/set方法
1、apache.commons.beanutils
Apache Commons BeanUtils 使用反射来访问和修改对象的属性。反射允许在运行时动态地获取类的信息,包括属性、方法等。这是实现对象克隆的核心。
在克隆对象时,BeanUtils 会:
- 遍历所有属性:获取源对象的所有属性描述符,遍历这些属性。
- 调用 Getter 方法:对于每个属性,检查是否存在可读的 getter 方法。如果存在,调用这个方法来获取属性值。
- 调用 Setter 方法:然后检查目标对象是否有相应的可写的 setter 方法。如果存在,就调用这个 setter 方法,将获取的属性值设置到目标对象。
如果没有标准的get/set方法会导致某些属性无法赋值
2、spring的BeanUtils
原理和apache.commons.beanutils类似,遍历每个属性描述符,检查其是否可读和可写。如果可以,就通过反射调用源对象的 getter 方法获取属性值,并通过目标对象的 setter 方法将该值设置到目标对象中。
Spring 在性能上进行了优化,例如通过缓存 PropertyDescriptor
来减少反射开销。虽然 Apache Commons BeanUtils 也在努力优化,但 Spring 的设计更注重高性能和灵活性。
- 类型转换:Spring 提供了更丰富的类型转换机制,可以在复制过程中自动进行类型转换。Apache Commons BeanUtils 也支持某些类型的转换,但功能可能没有 Spring 丰富。
- 深度复制:Spring 还支持深度复制(Deep Copy),即在复制复杂对象时,可以选择是否复制其内部的嵌套对象。Apache Commons BeanUtils 也支持某些嵌套属性,但在复杂场景下,Spring 提供了更全面的支持。
5、总结对比
方式 | 优点 | 缺点 |
---|---|---|
构造函数 | \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. 序列化与反序列化存在一定的系统开销,5.Java 序列化: 是 Java 提供的一种内建机制,原生序列化机制,通常不会导致数值精度丢失问题,在处理数值精度方面较为可靠。 |
Gson序列化 | \1. 可用性强,新增成员变量不需要修改拷贝方法 2. 对拷贝类没有要求,不需要实现额外接口和方法 | \1. 底层实现复杂 2. 需要引入Gson第三方JAR包 3. 序列化与反序列化存在一定的系统开销4、JSON 序列化: 将对象转换成 JSON 字符串,可能会导致数值精度丢失,特别是对于浮点数或大整数 |
Jackson序列化 | \1. 可用性强,新增成员变量不需要修改拷贝方法 | \1. 底层实现复杂 2. 需要引入Jackson第三方JAR包 3. 拷贝类(包括其成员变量)需要实现默认的无参构造函数 4. 序列化与反序列化存在一定的系统开销5.JSON 序列化: 将对象转换成 JSON 字符串,可能会导致数值精度丢失,特别是对于浮点数或大整数 |
apache.commons.beanutils反射get/set | 1、可用性强,实现标准get/set方法都可以拷贝 | 1.底层利用反射get/set方法实现复杂. 2、需要引入apache.commons第三方JAR包,3.拷贝类,所有字段需要有标准get/set方法,才能赋值属性,拷贝对象注意不同类型字段的转化,可能会导致数值精度丢失 |
spring的beanUtils | 1、可用性强,实现标准get/set方法都可以拷贝 | 1.底层利用反射get/set方法实现复杂. 2、需要引入spring第三方JAR包,3.拷贝类,所有字段需要有标准get/set方法,才能赋值属性,拷贝对象注意不同类型字段的转化,可能会导致数值精度丢失,4、对比apache.commons.beanutils提供更强大的类型转换和灵活的功能扩展 |
3、浅拷贝
浅拷贝创建一个新对象,并复制原始对象的所有非静态字段到新对象。但是,如果字段是引用类型,那么只复制引用而不复制引用的对象。因此,对于引用类型的字段,原始对象和新对象共享同一个内部对象。
通常可以通过创建一个新对象,并且使用构造函数或setter方法将原始对象的值复制到新对象中来实现浅拷贝。Java中的自动装箱和拆箱机制可以简化基本数据类型的复制。
创建了一个新的Person对象并且复制基本数据类型字段的值和引用类型字段的引用来实现浅拷贝。这意味着修改新对象的name字段不会影响原始对象,但修改新对象的address字段会影响原始对象,因为它们共享同一个Address对象。
class Person {
private String name;
private Address address;
// ... 其他属性和方法 ...
public Person shallowCopy() {
Person copy = new Person();
copy.setName(this.name); // 复制基本数据类型字段
copy.setAddress(this.address); // 只复制引用,不复制引用的对象
return copy;
}
}
4、零拷贝
零拷贝是一种在数据传输过程中避免不必要的数据复制的技术。零拷贝通常与I/O操作相关,尤其是当数据从一个存储位置移动到另一个存储位置时。通过直接在内存、文件或网络之间传输数据,零拷贝技术可以减少CPU的使用和内存带宽的消耗,从而提高性能。
Java中什么地方用到了零拷贝技术呢?比如
- MappedByteBuffer:使用内存映射文件将文件或文件的一部分映射到内存中,从而允许直接访问文件数据而不需要将数据复制到应用程序的内存中。这可以通过FileChannel中的map()方法实现。
- FileChannel的transferTo/transferFrom方法:这些方法允许数据直接在文件通道或套接字通道之间传输,而不需要先复制到应用程序的内存中。比如,可以使用FileChannel中的transferTo()方法将数据直接从文件发送到网络套接字。
- DirectBuffer:通过使用直接缓冲区(DirectBuffer),数据可以直接在操作系统的原生内存中进行处理,而不需要先复制到Java堆内存中。这可以通过创建一个ByteBuffer并调用其allocateDirect()方法来实现。
上面就是Java的NIO库提供了一些零拷贝技术的实现方法。
try (FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
FileChannel destinationChannel = new FileOutputStream("destination.txt").getChannel()) {
destinationChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
} catch (IOException e) {
e.printStackTrace();
}
在上面的代码中就使用FileChannel的transferFrom()方法实现了文件传输的零拷贝。数据直接从源文件通道传输到目标文件通道,而不需要先复制到应用程序的内存中。这种方法在处理大文件时可以提高性能并减少内存消耗。