Java学习之深拷贝浅拷贝及对象拷贝的两种思路

简介: 对象拷贝,是一个非常基础的内容了,为什么会单独的把这个领出来讲解,主要是先前遇到了一个非常有意思的场景有一个任务,需要解析类xml标记语言,然后生成document对象,之后将会有一系列针对document对象的操作通过实际的测试,发现生成Document对象是比较耗时的一个操作,再加上这个任务场景中,需要解析的xml文档是固定的几个,那么一个可以优化的思路就是能不能缓存住创建后的Document对象,在实际使用的时候clone一份出来

I. Java之Clone



0. 背景


对象拷贝,是一个非常基础的内容了,为什么会单独的把这个领出来讲解,主要是先前遇到了一个非常有意思的场景


有一个任务,需要解析类xml标记语言,然后生成document对象,之后将会有一系列针对document对象的操作


通过实际的测试,发现生成Document对象是比较耗时的一个操作,再加上这个任务场景中,需要解析的xml文档是固定的几个,那么一个可以优化的思路就是能不能缓存住创建后的Document对象,在实际使用的时候clone一份出来


1. 内容说明


看到了上面的应用背景,自然而言的就会想到深拷贝了,本篇博文则主要内容如下


  • 介绍下两种拷贝方式的区别
  • 深拷贝的辅助工具类
  • 如何自定义实现对象拷贝


II. 深拷贝和浅拷贝



0. 定义说明


深拷贝


相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,即两者的修改是隔离的,相互之间没有影响


浅拷贝


也是创建了一个对象,但是这个对象的某些内容(比如A)依然是被拷贝对象的,即通过这两个对象中任意一个修改A,两个对象的A都会受到影响


看到上面两个简单的说明,那么问题来了


  • 浅拷贝中,是所有的内容公用呢?还是某些内容公用?
  • 从隔离来将,都不希望出现浅拷贝这种方式了,太容易出错了,那么两种拷贝方式的应用场景是怎样的?


1. 浅拷贝


一般来说,浅拷贝方式需要实现Cloneable接口,下面结合一个实例,来看下浅拷贝中哪些是独立的,哪些是公用的


@Data
public class ShallowClone implements Cloneable {
    private String name;
    private int age;
    private List<String> books;
    public ShallowClone clone() {
        ShallowClone clone = null;
        try {
            clone = (ShallowClone) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
    public static void main(String[] args) {
        ShallowClone shallowClone = new ShallowClone();
        shallowClone.setName("SourceName");
        shallowClone.setAge(28);
        List<String> list = new ArrayList<>();
        list.add("java");
        list.add("c++");
        shallowClone.setBooks(list);
        ShallowClone cloneObj = shallowClone.clone();
        // 判断两个对象是否为同一个对象(即是否是新创建了一个实例)
        System.out.println(shallowClone == cloneObj);
        // 修改一个对象的内容是否会影响另一个对象
        shallowClone.setName("newName");
        shallowClone.setAge(20);
        shallowClone.getBooks().add("javascript");
        System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
        shallowClone.setBooks(Arrays.asList("hello"));
        System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
    }
}
复制代码


输出结果:

false
source: ShallowClone(name=newName, age=20, books=[java, c++, javascript])
clone:ShallowClone(name=SourceName, age=28, books=[java, c++, javascript])
source: ShallowClone(name=newName, age=20, books=[hello])
clone:ShallowClone(name=SourceName, age=28, books=[java, c++, javascript])
复制代码


结果分析:

  • 拷贝后获取的是一个独立的对象,和原对象拥有不同的内存地址
  • 基本元素类型,两者是隔离的(虽然上面只给出了int,String)
  • 基本元素类型包括:
  • int, Integer, long, Long, char, Charset, byte,Byte, boolean, Boolean, float,Float, double, Double, String
  • 非基本数据类型(如基本容器,其他对象等),只是拷贝了一份引用出去了,实际指向的依然是同一份


其实,浅拷贝有个非常简单的理解方式:


浅拷贝的整个过程就是,创建一个新的对象,然后新对象的每个值都是由原对象的值,通过 = 进行赋值


这个怎么理解呢?


上面的流程拆解就是:

- Object clone = new Object();
- clone.a = source.a
- clone.b = source.b
- ...
复制代码


那么=赋值有什么特点呢?


基本数据类型是值赋值;非基本的就是引用赋值


2. 深拷贝


深拷贝,就是要创建一个全新的对象,新的对象内部所有的成员也都是全新的,只是初始化的值已经由被拷贝的对象确定了而已


那么上面的实例改成深拷贝应该是怎样的呢?


可以加上这么一个方法

public ShallowClone deepClone() {
    ShallowClone clone = new ShallowClone();
    clone.name = this.name;
    clone.age = this.age;
    if (this.books != null) {
        clone.books = new ArrayList<>(this.books);
    }
    return clone;
}
// 简单改一下测试case
public static void main(String[] args) {
    ShallowClone shallowClone = new ShallowClone();
    shallowClone.setName("SourceName");
    shallowClone.setAge(new Integer(1280));
    List<String> list = new ArrayList<>();
    list.add("java");
    list.add("c++");
    shallowClone.setBooks(list);
    ShallowClone cloneObj = shallowClone.deepClone();
    // 判断两个对象是否为同一个对象(即是否是新创建了一个实例)
    System.out.println(shallowClone == cloneObj);
    // 修改一个对象的内容是否会影响另一个对象
    shallowClone.setName("newName");
    shallowClone.setAge(2000);
    shallowClone.getBooks().add("javascript");
    System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
    shallowClone.setBooks(Arrays.asList("hello"));
    System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
}
复制代码


输出结果为:

false
source: ShallowClone(name=newName, age=2000, books=[java, c++, javascript])
clone:ShallowClone(name=SourceName, age=1280, books=[java, c++])
source: ShallowClone(name=newName, age=2000, books=[hello])
clone:ShallowClone(name=SourceName, age=1280, books=[java, c++])
复制代码


结果分析:

  • 深拷贝独立的对象
  • 拷贝后对象的内容,与原对象的内容完全没关系,都是独立的


简单来说,深拷贝是需要自己来实现的,对于基本类型可以直接赋值,而对于对象、容器、数组来讲,需要创建一个新的出来,然后重新赋值


3. 应用场景区分


深拷贝的用途我们很容易可以想见,某个复杂对象创建比较消耗资源的时候,就可以缓存一个蓝本,后续的操作都是针对深clone后的对象,这样就不会出现混乱的情况了

那么浅拷贝呢?感觉留着是一个坑,一个人修改了这个对象的值,结果发现对另一个人造成了影响,真不是坑爹么?


假设又这么一个通知对象长下面这样


private String notifyUser;
// xxx
private List<String> notifyRules;
复制代码


我们现在随机挑选了一千个人,同时发送通知消息,所以需要创建一千个上面的对象,这些对象中呢,除了notifyUser不同,其他的都一样


在发送之前,突然发现要临时新增一条通知信息,如果是浅拷贝的话,只用在任意一个通知对象的notifyRules中添加一调消息,那么这一千个对象的通知消息都会变成最新的了;而如果你是用深拷贝,那么苦逼的得遍历这一千个对象,每个都加一条消息了


III. 对象拷贝工具



上面说到,浅拷贝,需要实现Clonebale接口,深拷贝一般需要自己来实现,那么我现在拿到一个对象A,它自己没有提供深拷贝接口,我们除了主动一条一条的帮它实现之外,有什么辅助工具可用么?


对象拷贝区别与clone,它可以支持两个不同对象之间实现内容拷贝


Apache的两个版本:(反射机制)

org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)
org.apache.commons.beanutils.BeanUtils#cloneBean
复制代码


Spring版本:(反射机制)

org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)
复制代码


cglib版本:(使用动态代理,效率高)

net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)
复制代码


上面的几个有名的工具类来看,提供了两种使用者姿势,一个是反射,一个是动态代理,下面分别来看两种思路


1. 借助反射实现对象拷贝


通过反射的方式实现对象拷贝的思路还是比较清晰的,先通过反射获取对象的所有属性,然后修改可访问级别,然后赋值;再获取继承的父类的属性,同样利用反射进行赋值


上面的几个开源工具,内部实现封装得比较好,所以直接贴源码可能不太容易一眼就能看出反射方式的原理,所以简单的实现了一个, 仅提供思路


public static void copy(Object source, Object dest) throws Exception {
    Class destClz = dest.getClass();
    // 获取目标的所有成员
    Field[] destFields = destClz.getDeclaredFields();
    Object value;
    for (Field field : destFields) { // 遍历所有的成员,并赋值
        // 获取value值
        value = getVal(field.getName(), source);
        field.setAccessible(true);
        field.set(dest, value);
    }
}
private static Object getVal(String name, Object obj) throws Exception {
    try {
        // 优先获取obj中同名的成员变量
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    } catch (NoSuchFieldException e) {
        // 表示没有同名的变量
    }
    // 获取对应的 getXxx() 或者 isXxx() 方法
    name = name.substring(0, 1).toUpperCase() + name.substring(1);
    String methodName = "get" + name;
    String methodName2 = "is" + name;
    Method[] methods = obj.getClass().getMethods();
    for (Method method : methods) {
        // 只获取无参的方法
        if (method.getParameterCount() > 0) {
            continue;
        }
        if (method.getName().equals(methodName)
                || method.getName().equals(methodName2)) {
            return method.invoke(obj);
        }
    }
    return null;
}
复制代码


上面的实现步骤还是非常清晰的,首先是找同名的属性,然后利用反射获取对应的值


Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
复制代码


如果找不到,则找getXXX, isXXX来获取


2. 代理的方式实现对象拷贝


Cglib的BeanCopier就是通过代理的方式实现拷贝,性能优于反射的方式,特别是在大量的数据拷贝时,比较明显


代理,我们知道可以区分为静态代理和动态代理,简单来讲就是你要操作对象A,但是你不直接去操作A,而是找一个中转porxyA, 让它来帮你操作对象A


那么这种技术是如何使用在对象拷贝的呢?


我们知道,效率最高的对象拷贝方式就是Getter/Setter方法了,前面说的代理的含义指我们不直接操作,而是找个中间商来赚差价,那么方案就出来了


将原SourceA拷贝到目标DestB


  • 创建一个代理 copyProxy
  • 在代理中,依次调用 SourceA的get方法获取属性值,然后调用DestB的set方法进行赋值


实际上BeanCopier的思路大致如上,具体的方案当然就不太一样了, 简单看了一下实现逻辑,挺有意思的一块,先留个坑,后面单独开个博文补上


说明


从实现原理和通过简单的测试,发现BeanCopier是扫描原对象的getXXX方法,然后赋值给同名的 setXXX 方法,也就是说,如果这个对象中某个属性没有get/set方法,那么就无法赋值成功了


IV. 小结



1. 深拷贝和浅拷贝


深拷贝


相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,即两者的修改是隔离的,相互之间没有影响


  • 完全独立


浅拷贝


也是创建了一个对象,但是这个对象的某些内容(比如A)依然是被拷贝对象的,即通过这两个对象中任意一个修改A,两个对象的A都会受到影响


  • 等同与新创建一个对象,然后使用=,将原对象的属性赋值给新对象的属性
  • 需要实现Cloneable接口


2. 对象拷贝的两种方法


通过反射方式实现对象拷贝


主要原理就是通过反射获取所有的属性,然后反射更改属性的内容


通过代理实现对象拷贝


将原SourceA拷贝到目标DestB

创建一个代理 copyProxy 在代理中,依次调用 SourceA的get方法获取属性值,然后调用DestB的set方法进行赋值


V. 其他



声明


尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见解不全,如有问题,欢迎批评指正



相关文章
|
28天前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
86 43
Java学习十六—掌握注解:让编程更简单
|
20天前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
23天前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
43 17
|
13天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
13天前
|
消息中间件 缓存 Java
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
零拷贝技术 Zero-Copy 是指计算机执行操作时,可以直接从源(如文件或网络套接字)将数据传输到目标缓冲区, 而不需要 CPU 先将数据从某处内存复制到另一个特定区域,从而减少上下文切换以及 CPU 的拷贝时间。
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
|
23天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
21天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
1月前
|
存储 Java 数据管理
Java零基础-Java对象详解
【10月更文挑战第7天】Java零基础教学篇,手把手实践教学!
25 6
|
23天前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
22 0
|
6月前
|
Java Apache
Java中的深拷贝与浅拷贝
Java中的深拷贝与浅拷贝
45 0