Java中的深拷贝与浅拷贝探究(利用反射+泛型实现深拷贝工具类)
前提
为了降低演示的代码量,实体类属性的get,set等方法通过lombok的Data注解实现。
要引入lombok注解,项目需要是maven项目才行。普通项目还是手动写get,set等方法吧。
想直接看深拷贝工具类实现的点这里。
引入依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
安装插件:
idea左上角,文件/设置/插件,搜索lombok然后下载。
目录
一、浅拷贝
概念
浅拷贝的效果是创建一个新的对象,然后将原对象内部的全部属性都赋值到新对象,从而得到一个拷贝来的新对象。
效果就是赋值,如果属性是基本类型(int,double等)则值拷贝,引用类型则地址拷贝,也就意味着共用对象。
浅拷贝得到的对象,除了地址和原对象不一样,内部完全一样,也就是说引用类型就共用对象。
tips:赋值不是浅拷贝,浅拷贝是会得到一个新对象的。
实现方案
1.使用Object类的clone方法
需要浅拷贝的类需要实现Cloneable接口,重写Object类的clone方法,重写的clone方法内调用父类的clone就行,并设置方法的访问级别为public。
User类:
import lombok.Data;
/**
* @ClassName: User
* @Author:
* @Date: 2024/10/30 14:33
* @Description:
**/
@Data
public class User implements Cloneable {
private String id;
private String name;
private Integer age;
private Children children;
@Override
public User clone() {
try {
return (User) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
Children类:
import lombok.Data;
/**
* @ClassName: chidren
* @Author:
* @Date: 2025/1/18 14:43
* @Description:
**/
@Data
public class Children {
private String id;
}
测试类:
public class MapTest {
public static void main(String[] args) {
User user=new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setId("123");
user.setChildren(children);
User cloneUser= user.clone();
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user==cloneUser);
System.out.println(user.equals(cloneUser));
Children cloneUserChildren = cloneUser.getChildren();
System.out.println(children==cloneUserChildren);
System.out.println(children.equals(cloneUserChildren));
}
}
测试结果:
如下图,从前两行对象打印来看,对象内部结构是完全一样的。
第三行打印结果是false,说明克隆出来的对象确实是一个新对象。
第四行打印结果是true,说明他们虽然不是同一个对象,但从对象的结构各方面判断他们是一样的。
第五第六行打印结果都是true,说明他们内部的children对象就是同一个对象,也就是共用对象,如果修改原对象的children的id值,克隆对象的children的id值也会跟着变。
2.一个一个拷贝对象的属性(现成的工具类很多)
这种方法简单得多,可以在实体类新建一个浅拷贝方法,内部新建一个对象,然后将该对象的属性都复制给新对象并返回,但这样做灵活性低,后续新增字段的时候还需要改浅拷贝方法。
可以考虑使用反射机制来获取该对象的全部属性,然后根据属性名,将属性值一个一个赋值给新对象。现成的就有很多工具类实现了该功能。如org.apache.commons.beanutils.BeanUtils可以直接浅拷贝对象,而hutool的BeanUtil,org.springframework.beans.BeanUtils等提供了复制全部属性的方法,自己创建对象,然后将对象传入也可以实现浅拷贝。
反射机制:反射机制允许Java程序在运行的过程中动态获取类的所有方法、属性,通过类对象可以获取对象的属性值,还可以设置对象的属性值。
下面以org.apache.commons.beanutils.BeanUtils为例实现。
User类:
import lombok.Data;
@Data
public class User {
private String id;
private String name;
private Integer age;
private Children children;
}
Children类:
import lombok.Data;
@Data
public class Children {
private String id;
}
测试类:
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
public class MapTest {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
User user=new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setId("123");
user.setChildren(children);
User cloneUser= (User) BeanUtils.cloneBean(user);
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user==cloneUser);
System.out.println(user.equals(cloneUser));
Children cloneUserChildren = cloneUser.getChildren();
System.out.println(children==cloneUserChildren);
System.out.println(children.equals(cloneUserChildren));
}
}
测试结果:
如下图,和上一次的浅拷贝结果完全一样,需要分析的也看上面。
反射实现对象属性拷贝原理
反射机制非常方便,在实际项目开发中用到的地方很多。比如动态代理,excel的POI等。
User类:
import lombok.Data;
@Data
public class User {
private String id;
private String name;
private Integer age;
private Children children;
}
Children类:
import lombok.Data;
@Data
public class Children {
private String id;
}
测试方法:
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class MapTest {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException {
User user=new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setId("123");
user.setChildren(children);
Class<? extends User> userClass = user.getClass();
Field[] fields = userClass.getDeclaredFields();//获取该类的全部定义的属性
User cloneUser= userClass.newInstance();//通过获取到的类创建对象
for(Field field:fields){
field.setAccessible(true);
Object o = field.get(user);//取原对象的值
field.set(cloneUser,o);//设置新对象的值
field.setAccessible(false);
}
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user==cloneUser);
System.out.println(user.equals(cloneUser));
Children cloneUserChildren = cloneUser.getChildren();
System.out.println(children==cloneUserChildren);
System.out.println(children.equals(cloneUserChildren));
}
}
测试结果:
和上面一样的。
二、深拷贝
概念
深拷贝的效果也是创建一个新的对象,如果属性是基本类型则值拷贝,如果属性是引用类型,则创建新的对象赋值。并且需要递归整个对象去拷贝。
深拷贝得到的对象是一个完全新的对象,对新对象进行操作或者对原对象进行操作都不会互相影响。
实现方案
1.递归调用Object类的clone方法
这种方式是将重写的clone方法实现为一个深拷贝实现。
首先类及其内部属性涉及到的类都需要实现Cloneable接口并重写clone方法,接着在重写的clone方法中,对引用类型的属性都需要调用该引用对象的clone方法进行拷贝并赋值。这样得到的才是一个完全新的深拷贝对象。
User类:
import lombok.Data;
@Data
public class User implements Cloneable {
private String id;
private String name;
private Integer age;
private Children children;
@Override
public User clone() {
User user=null;
try {
user= (User) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
if(user!=null){
user.children=children.clone();
}
return user;
}
}
Children类:
import lombok.Data;
@Data
public class Children implements Cloneable{
private String id;
@Override
public Children clone(){
try {
return (Children) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
测试代码:
import java.lang.reflect.InvocationTargetException;
public class MapTest {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException {
User user=new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setId("123");
user.setChildren(children);
User cloneUser= user.clone();
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user==cloneUser);
System.out.println(user.equals(cloneUser));
Children cloneUserChildren = cloneUser.getChildren();
System.out.println(children==cloneUserChildren);
System.out.println(children.equals(cloneUserChildren));
}
}
测试结果:
一样的结果就不额外说明了,只说倒数第二行输出,原children和拷贝的children进行地址比较,结果是false,说明拷贝对象内部的引用类型也是新对象了。
存在的问题:
按照这种实现方式,每次增加新的属性就都需要修改clone方法,并且子类也都需要实现clone方法,维护成本高。
2.利用反射机制递归拷贝引用属性外加泛型增加通用性(有缺陷)
这种方式算是第一种的改进版,但该方案不限定浅拷贝的实现方式。总的实现方案就是在浅拷贝方法内部返回对象之前,递归该对象内部的引用属性并创建新对象返回。再将方法参数定义为泛型,增加方法的通用性。
该方式还有缺点,面对定义为final的属性,在创建对象的时候就已经需要给定值了,如果根据构造函数不同给不同的值,就没法知道调用哪个构造函数赋值是正确的,也就没法准确给该final属性赋值。
目前的实现不支持拷贝数组类型的数据,无法处理循环依赖。
User类:
import lombok.Data;
@Data
public class User{
private String id;
private String name;
private Integer age;
private Children children;
}
Children类:
import lombok.Data;
import java.util.Date;
@Data
public class Children{
private String id;
private Date date;
}
测试代码:
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Date;
public class MapTest {
public static <T> T deepCopy(T obj) throws IllegalAccessException, InstantiationException {
if (obj == null) {
return obj;
}
Class<?> aClass = obj.getClass();
T newObj = (T) aClass.newInstance();
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
Class<?> fieldType = field.getType();
Object o = field.get(obj);
if (o == null) {
field.setAccessible(false);
continue;
}
if (fieldType == String.class || fieldType == int.class || fieldType == Integer.class
|| fieldType == boolean.class || fieldType == Boolean.class || fieldType == double.class
|| fieldType == Double.class || fieldType == float.class || fieldType == Float.class||fieldType==long.class||fieldType==Long.class) {
field.set(newObj, o);
} else {
field.set(newObj, deepCopy(o));
}
}
return newObj;
}
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException {
User user = new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setId("123");
children.setDate(new Date());
user.setChildren(children);
User cloneUser = null;
cloneUser = MapTest.deepCopy(user);
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user == cloneUser);
System.out.println(user.equals(cloneUser));
Children userChildren = cloneUser.getChildren();
System.out.println(children==userChildren);
System.out.println(children.equals(userChildren));
}
}
测试结果:
抛异常了,看解释似乎是没有权限啥的。在网上搜,搜出来都是说加field.setAccessible(true);但我的代码已经有了,看来只能靠自己了。
找到具体报错的代码,T newObj = (T) aClass.newInstance();调试发现是创建sun.util.calendar.Gregorian的时候报错。当时在想会不会因为这个属性是常量导致的,但仔细一想,常量只是不能重新赋值而已,现在还在创建对象阶段。把这个类拿出来创建对象,然后发现创建不了,这时候报错才明显。
进入到该类一看。才发现Gregorian的构造方法是非公有的,这才想到应该要给构造函数开放权限。
因为Class.newInstance方法底层就是调用无参构造函数创建对象的,因此手动设置无参构造函数的访问权限,并直接用构造函数创建对象。
Class<?> aClass = obj.getClass();
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
T newObj = (T) declaredConstructor.newInstance();
declaredConstructor.setAccessible(false);
Field[] declaredFields = aClass.getDeclaredFields();
无法接入类的问题解决了,然后出现了一个新的异常。不过这个异常很好看懂,就是不能给gcal赋值,为啥呢,因为这是一个final加static修饰的属性,也就是常量,在初始化静态变量的时候值就定下来了,就不能改了。
因此在遇到final修饰的属性,都可以直接跳过了。遇到static修饰的变量也直接跳,反正是类维度共享的。至此得到了最终版深拷贝工具类实现。
利用反射实现深拷贝最终版(对反射、循环依赖好奇的推荐了解一下)
该版本支持了数组类型,采用记录拷贝过的对象来解决循环依赖问题,和Spring处理循环依赖思路类似,都是先创建一个空的对象,然后将该对象缓存起来,然后初始化属性,如果属性中其他对象需要注入则取出空对象注入。(不过没找到除依赖注入之外的创建循环依赖对象的方法)
该版本的缺陷还是无法处理final修饰的属性。
import cn.hutool.core.util.ObjectUtil;
import java.lang.reflect.*;
import java.util.*;
public class MapTest {
//防止循环依赖
public static ThreadLocal<Map<Object, Object>> threadLocal = new ThreadLocal<>();
public static <T> T deepCopy(T obj) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
if (obj == null) {
return obj;
}
Map<Object, Object> objectObjectMap = threadLocal.get();
if (ObjectUtil.isNull(objectObjectMap)) {
objectObjectMap = new HashMap<>();
}
if (objectObjectMap.containsKey(obj)) {
return (T) objectObjectMap.get(obj);
}
Class<?> fieldType = obj.getClass();
if (fieldType == String.class || fieldType == int.class || fieldType == Integer.class
|| fieldType == boolean.class || fieldType == Boolean.class || fieldType == double.class
|| fieldType == Double.class || fieldType == float.class || fieldType == Float.class || fieldType == long.class || fieldType == Long.class) {
return obj;
}
T newObj = null;
if (fieldType.isArray()) {
int length = Array.getLength(obj);
T newArray = (T) Array.newInstance(obj.getClass().getComponentType(), length);
objectObjectMap.put(obj, newArray);
threadLocal.set(objectObjectMap);
for (int i = 0; i < length; i++) {
Array.set(newArray, i, deepCopy(Array.get(obj, i)));
}
return newArray;
} else {
Constructor<?> declaredConstructor = fieldType.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
newObj = (T) declaredConstructor.newInstance();
declaredConstructor.setAccessible(false);
objectObjectMap.put(obj, newObj);
threadLocal.set(objectObjectMap);
Field[] declaredFields = fieldType.getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
Object o = field.get(obj);
int modifiers = field.getModifiers();
if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
field.setAccessible(false);
continue;
}
field.set(newObj, deepCopy(o));
}
}
return newObj;
}
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException {
User user = new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setDate(new Date());
children.setId("123");
user.setChildren(children);
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
user.setScore(list);
User cloneUser = deepCopy(user);
list.add(3);
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user == cloneUser);
System.out.println(user.equals(cloneUser));
Children cloneUserChildren = cloneUser.getChildren();
System.out.println(children == cloneUserChildren);
System.out.println(children.equals(cloneUserChildren));
}
}
测试结果:因为增加了数组类型,因而改变了测试的方式,拷贝完之后往user里面的list插入数据,拷贝出来的对象内部的list没有变化,深拷贝成功
3.序列化实现深拷贝(企业开发的推荐这个)
该方式要求深拷贝的类及其子类都要实现序列化接口,在企业中更多也是使用这个。本质上就是将对象转成二进制字节流再转回对象。
使用简单,有现成的工具类。支持final修饰的字段。
使用序列化进行深拷贝会丢失transient修饰的属性值。
性能消耗大。
User类:
import lombok.Data;
@Data
public class User implements Serializable {
private String id;
private String name;
private Integer age;
private Children children;
}
Children类:
import lombok.Data;
import java.util.Date;
@Data
public class Children implements Serializable {
private String id;
private Date date;
}
测试代码:
方式一,使用第三方库(更稳定)
使用org.apache.commons.lang3.SerializationUtils的clone方法实现深拷贝。
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Date;
import org.apache.commons.lang3.SerializationUtils;
public class MapTest {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException {
User user=new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setDate(new Date());
children.setId("123");
user.setChildren(children);
children.setUser(user);
BigDecimal decimal;
User cloneUser= null;
cloneUser= SerializationUtils.clone(user);
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user==cloneUser);
System.out.println(user.equals(cloneUser));
Children cloneUserChildren = cloneUser.getChildren();
System.out.println(children==cloneUserChildren);
System.out.println(children.equals(cloneUserChildren));
Class<Gregorian> c=Gregorian.class;
}
}
测试结果:深拷贝成功
方式二,存储文件再读取
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Date;
public class MapTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User();
user.setId("10");
user.setAge(180);
user.setName("lili");
Children children = new Children();
children.setId("123");
children.setDate(new Date());
user.setChildren(children);
User cloneUser = null;
ObjectOutput objectOutput=new ObjectOutputStream(new FileOutputStream("user.txt"));
objectOutput.writeObject(user);
objectOutput.flush();
ObjectInputStream objectInput=new ObjectInputStream(new FileInputStream("user.txt"));
cloneUser =(User) objectInput.readObject();
System.out.println(user);
System.out.println(cloneUser);
System.out.println(user == cloneUser);
System.out.println(user.equals(cloneUser));
Children userChildren = cloneUser.getChildren();
System.out.println(children==userChildren);
System.out.println(children.equals(userChildren));
}
}
测试结果:深拷贝成功
4.转JSON再解析成对象(不推荐)
将对象转成JSON再转回对象是一种非常简单的实现方式,如果自己简单玩玩可以试试,但存在较多问题。
例如类型丢失,转成JSON结构的时候,同样是数字就区分不出属于什么类型,特别是用Object类来接受对象输入的时候。
精度丢失,像大数类型。
这个本质上就是JSON转对象和对象转JSON字符串,感兴趣可以看看这篇文章。
JSON和对象互转
三、总结
浅拷贝很简单,而且实现的工具类也很多就不深究了。
深拷贝的实现思路主要分为两种,一种是递归拷贝,一种是序列化。
- 递归拷贝的,比较难处理的是final修饰却不是常量的属性,需要在构造函数进行初始化。如果是自己的库实现,可以在类创建一个深拷贝方法,内部调用有参构造函数完成final属性的初始化。基本思路是这样,但无法处理第三方的类,并且维护成本高。
如果采用泛型+反射实现,虽然不需要繁琐的每个类都创建深拷贝方法,但无法处理final修饰的属性。 - 通过序列化实现深拷贝存在的问题就是无法拷贝transient修饰的属性,需要特殊处理,如果是final+transient修饰的,就不好处理了,又回到上面的问题。
目前有org.apache.commons.lang3.SerializationUtils;的实现和hutool的ObjectUtil.clone(T obj);经过测试,序列化的方式可以处理final修饰的属性。
想要递归结合序列化,也无法完全解决上面的问题,因为可能存在final+transient修饰的属性。
因为一般需要深拷贝的都是业务写的实体类,因此可以看情况考虑,如果对象内基本不会定义单纯final的属性,可以考虑使用递归的方法;如果实体类都实现了序列化接口,并且不在乎transient修饰的属性,则可以考虑序列化。(在乎transient修饰的属性可以考虑额外处理),就目前来看,企业级开发使用序列化方案的更多。
关于能否解决循环依赖问题,按照思路来说是没问题的,但Java是有自动检测循环依赖的能力的,因此在业务的实体类上基本不会出现循环依赖的设计,就不需要过于担心。
原文地址:https://blog.csdn.net/weixin_43975276/article/details/145227961
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!