Java中实现对象的拷贝案例讲解

2025-09-23 12:50

本文主要是介绍Java中实现对象的拷贝案例讲解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Java中实现对象的拷贝案例讲解》Java对象拷贝分为浅拷贝(复制值及引用地址)和深拷贝(递归复制所有引用对象),常用方法包括Object.clone()、序列化及JSON转换,需处理循环引用问题,...

对象的拷贝

简介

对象的拷贝,把一个对象中的数据,复制到另一个相同类型的对象中,在某些情况下,这会很有用,例如,创建线程安全的实例中提到的一个规范,对象应该返回自身数据的拷贝,而不应该返回数据本身,避免外部对于数据的修改影响到对象本身,String类就是这么写的,它会对外返回char数组的拷贝,而不是char数组本身,这样,当用户修改char数组时,不会影响到String对象本身。

String类的toCharArray方法,返回内部char数组的拷贝

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

浅拷贝和深拷贝

对象的拷贝,依据类型的不同,可以分为浅拷贝和深拷贝,浅拷贝是引用复制,深拷贝是数据复制。

浅拷贝和深拷贝:

  • 浅拷贝:将原对象中的所有字段复制到新对象中,如果字段是基本数据类型,则复制的是值,如果字段是引用数据类型,则复制的是指向引用的地址,此时,新对象和原对象之间存在共享数据,如果某个对象修改了这部分数据,另一个对象也会受影响
  • 深拷贝:不仅将源对象中所有字段复制到新对象中,还会递归地复制字段引用的对象,在深拷贝结束后,原对象和新对象之间不存在任何共享数据,某个对象被修改,不会影响到另一个对象。

浅拷贝

Object.clone方法,浅拷贝通常是通过这个方法来实现,它是一个本地方法,位于Object类中,子类也可以重写它来实现深拷贝,但是通常不需要这么做,因为代价比较大,如果希望深拷贝是一项通用能力的话。

方法签名:protected native Object clone() throws CloneNotSupportedException;

clone方法的使用:

  • clone()方法是protected的,这意味着只有子类和同一个包中的其他类可以调用它。
  • 如果一个类没有实现Cloneable接口,尝试调用它的clone()方法将抛出CloneNotSupportedException。
  • 当用需要克隆一个对象时,需要会在子类中重写clone()方法,并确保它是public的,这样外部代码才能调用它。

clone方法的底层原理:clone()方法创建了一个对象的浅拷贝。这意味着新对象和原始对象在内存中是两个独立的副本,但是它们中的引用字段指向相同的对象。

案例:

// Person类
public class Person implements Cloneable{
    private String name;
    private int age;
    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
    // 省略构造方法、getter、setter、toString
}
// PersonGroup类:持有Person类的引用,通过这个引用,就可以看出深拷贝和浅拷贝的区别
public class PersonGroup implements Cloneable {
    private String groupName;
    private Person person1;
    @Override
    public PersonGroup clone() throws CloneNotSupportedException {
        return (PersonGroup) super.clone();
    }
    // 省略构造方法、getter、setter、toString
}
// 测试
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person("张三", 18);
        PersonGroup personGroup = new PersonGroup("aaa", person);
        PersonGroup clone = personGroup.clone();
        System.out.println("clone = " + clone);
    }
}

对测试类进行debug,观察克隆出的实例和原实例的内存地址,以及它们中成员变量的内存地址,可以得出结论:Object类中的clone方法,将原实例在堆内存中的数据复制了一份,到新的内存空间,再将新内存空间的地址赋值给clone后的对象,两块内存地址中如果有引用类型的数据,那么它们指向相同的内存空间

关于浅拷贝,有一个地方值得注意,在原对象中有引用类型的数据的情况下,浅拷贝出的新对象,如果修改了这个引用指向的对象,原对象也会受影响,如果是修改了引用本身的指向,那么新对象不受影响。

深拷贝

深拷贝有多种实现方式,包括基于IO流的深拷贝、基于json转换的深拷贝,核心原理都是把原对象和它引用的对象,全部复制一份,然后创建一个新对象,新对象的引用也是新对象。原对象和新对象中不存在任何共享对象。

方式1:使用IO流实现深拷贝

@SuppressWarnings("unchecked")
public static <T> Tjs deepCopyByIO(T object) {
    if (object == null) {
        return null;
    }
    // ByteArrayOutputStream、ByteArrayInputStream,是内存流,即使不关闭也不会造成资源泄露,例如文件句柄未
    // 释放等问题,即使如此,随时关闭流依然是一个好习惯,而且关闭输出流会保证缓冲区的数据被正确的写出
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
        objectOutputStream.writeObject(object);
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
             ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
            return (T) objectInputStream.readObject();
        }
    } catch (Exception e) {
        e.printStackTrace(); // 打印异常信息
        return null;
    }
}

apache提供的common-langs包中也提供了类似的实现,SerializationUtils.clone方法

2、使用json实现深拷贝

// 这里使用的是gson框架,
public static <T> T deepCopyByGson(T object) {
    if (object == null) {
        return null;
    }
    try {
        Gson gson = new Gson(); 
        // 获取对象的类型
        Type type = object.getClass();
        // 将对象转换为 JSON 字符串
        String jsonString = gson.toJson(object);
        // 从 JSON 字符串解析回对象
        return gson.fromJson(jsonString, type);
    } catch (Exception e) {
        e.printSCNpLGCVCtackTrace();
        return null;
    }
}

3、使用一些更高http://www.chinasem.cn效的序列化框架,如hession、kryo等,都可以实现深拷贝。

深拷贝和循环引用

循环引用是指,两个对象互相持有对方的引用,这种情况下,即使是简单的toString方法,如果处理不当,也可能会栈内存溢出。

案例:循环引用

// 两个对象互相持有对方的引用
@Getter
@Setter
public static class Person1 implements Serializable {
    private String name;
    private Person2 person2;
    @Override
    public String toString() {
        return "Person1{" +
                "name='" + name + '\'' +
                ", person2=" + person2 +
              http://www.chinasem.cn  '}';
    }
}
@Getter
@Setter
public static class Person2 implements Serializable {
    private String name;
    private Person1 person1;
    @Override
    public String toString() {
        return "Person2{" +
                "name='" + name + '\'' +
                ", person1=" + person1.toString() +
                '}';
    }
}
// 测试
@Test
public void test4() {
    Person1android person1 = new Person1();
    Person2 person2 = new Person2();
    person1.setName("zs");
    person1.setPerson2(person2);
    person2.setName("ls");
    person2.setPerson1(person1);
    System.out.println("person1 = " + person1);
}

这个案例会报栈内存溢出,因为在打印对象时,调用了它的toString方法,打印对象1的时候发现需要打印对象2,打印对象2的时候发现需要打印对象1,造成循环调用,最终栈内存溢出。

所以在深拷贝时,如何解决循环引用的问题?

方式1:使用基于io流的方式,这种方法天然可以避免循环引用,因为对象序列化机制内置了对循环引用的检测和处理

方式2:忽略循环引用相关的字段,某些json框架可以手动配置,忽略某些字段。

案例:使用fastjson来解决循环引用的问题,这是fastjson内置的特性,在最终结果中,它会使用 “ref” 来代替循环引用

@Test
public void test4() {
    Person1 person1 = new Person1();
    Person2 person2 = new Person2();
    person1.setName("zs");
    person1.setPerson2(person2);
    person2.setName("ls");
    person2.setPerson1(person1);
    // 深拷贝
    String jsonString = JSON.toJSONString(person1, JSONWriter.Feature.ReferenceDetection);
    Person1 person3 = JSON.parseobject(jsonString, Person1.class);
    // {"name":"zs","person2":{"name":"ls","person1":{"$ref":"$"}}}
    System.out.println("jsonString = " + jsonString);
}

总结

这里介绍了复制一个对象的方法,包括浅拷贝、深拷贝,以及它们的基本使用

到此这篇关于Java中如何实现对象的拷贝的文章就介绍到这了,更多相关java对象的拷贝内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于Java中实现对象的拷贝案例讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155981

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三