当前位置: 首页 > article >正文

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然后下载。
在这里插入图片描述

目录

  • 前提
  • 一、浅拷贝
    • 概念
    • 实现方案
      • 1.使用Object类的clone方法
      • 2.一个一个拷贝对象的属性(现成的工具类很多)
      • 反射实现对象属性拷贝原理
  • 二、深拷贝
    • 概念
    • 实现方案
      • 1.递归调用Object类的clone方法
        • 存在的问题:
      • 2.利用反射机制递归拷贝引用属性外加泛型增加通用性(有缺陷)
      • 利用反射实现深拷贝最终版(对反射好奇的推荐了解一下)
      • 3.序列化(企业开放的推荐这个)
        • 方式一,使用第三方库(更稳定)
        • 方式二,存储文件再读取
      • 4.转JSON再解析成对象(不推荐)

一、浅拷贝

概念

浅拷贝的效果是创建一个新的对象,然后将原对象内部的全部属性都赋值到新对象,从而得到一个拷贝来的新对象。
效果就是赋值,如果属性是基本类型(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修饰的变量也直接跳,反正是类维度共享的。至此得到了最终版深拷贝工具类实现。

利用反射实现深拷贝最终版(对反射好奇的推荐了解一下)

存在final并且非静态的对象依旧无法确定该用哪个构造函数。

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Date;
/**
 * @ClassName: MapTest
 * @Author: 
 * @Date: 2025/1/18 14:11
 * @Description:
 **/
public class MapTest {
    public static <T> T deepCopy(T obj) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        if (obj == null) {
            return obj;
        }
        Class<?> aClass = obj.getClass();
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        T newObj = (T) declaredConstructor.newInstance();
        declaredConstructor.setAccessible(false);
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            field.setAccessible(true);
            boolean isFinal = Modifier.isFinal(field.getModifiers());
            boolean aStatic = Modifier.isStatic(field.getModifiers());
            if(isFinal||aStatic){
                //final修饰或者static修饰的属性,跳过
                field.setAccessible(false);
                continue;
            }
            Class<?> fieldType = field.getType();
            Object o = field.get(obj);
            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));
    }
}

测试结果:深拷贝成功
在这里插入图片描述

期间遇到一个问题,在深拷贝Date对象的时候,内部有一个类无法创建对象。

3.序列化(企业开放的推荐这个)

该方式要求深拷贝的类及其子类都要实现序列化接口,在企业中更多也是使用这个。本质上就是将对象转成二进制字节流再转回对象。

属性的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 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;
        cloneUser =SerializationUtils.clone(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));
    }
}

测试结果:深拷贝成功
在这里插入图片描述

方式二,存储文件再读取
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和对象互转


http://www.kler.cn/a/509358.html

相关文章:

  • 谷歌宣布没 JavaScript 将无法启动搜索,居然引起了轩然大波
  • 蓝桥杯 Python 组知识点容斥原理
  • SpringMVC 实战指南:打造高效 Web 应用的秘籍
  • 计算机网络 (41)文件传送协议
  • opencv projectPoints函数 computeCorrespondEpilines函数 undistortPoints函数
  • NumPy;NumPy在数据分析中的应用;NumPy与其他库的搭配使用
  • iOS - Objective-C 底层实现中的哈希表
  • UiPath发送嵌入图片HTML邮件
  • BGP联盟
  • 窗口门狗实验(WWDG)实验【学习】
  • 【高阶数据结构】位图(BitMap)
  • OSPF - 路由过滤的几种方法
  • C++/QT环境下图像在窗口下等比例渲染绘制
  • OpenEuler学习笔记(一):常见命令
  • UDP 单播、多播、广播:原理、实践
  • 【C++笔记】红黑树封装map和set深度剖析
  • 高性能、并发安全的 Go 嵌入式缓存库 如何使用?
  • 浅谈云计算22 | Kubernetes容器编排引擎
  • ASP.NET Core全球化与本地化:打造多语言应用
  • vulnhub靶场【jangow】靶机,考察反弹shell的流量及端口的选择
  • Transformer之Encoder
  • 如何在openEuler中编译安装Apache HTTP Server并设置服务管理(含Systemd和Init脚本)
  • 【Linux】线程全解:概念、操作、互斥与同步机制、线程池实现
  • linux下springboot项目nohup日志或tomcat日志切割处理方案
  • Redis集群部署详解:主从复制、Sentinel哨兵模式与Cluster集群的工作原理与配置
  • leetcode707-设计链表