java 深拷贝探讨

2024-09-05 12:18
文章标签 java 拷贝 探讨

本文主要是介绍java 深拷贝探讨,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

java 深拷贝探讨

本文将讨论以下4个问题

    1. java Cloneable接口实现深拷贝2. java 序列化实现深拷贝3. 号称最快的深拷贝二方库cloning源码分析4. 几种拷贝方式速度的比较

深拷贝的概念本文就不说了。在C++中实现深拷贝一般情况下重载赋值操作符 “=” 来实现同一个类的对象间的深拷贝,所以很自然的在java中我们也同样可以定义一个copy函数,在函数内部为对象的每一个属性作赋值操作。这种方式简单自然,但存在一个致命性的问题:如果有一天在类中新增加了一个需要深拷贝的属性,那么相应的copy函数也得进行修改,这种方法给类的可扩展性带来了极大的不方便。怎么解决这种问题,且看接下来的1、2、3章节的实现方式和4节的速度测试。

1. java Cloneable接口实现深拷贝

这种方式,需要类实现Colneable接口 clone 函数,在clone函数中调用super.clone。这种方式的深拷贝同样会带来另一个问题,如果类中有其他类的对象作为属性,则其他的类也需要重载并实现Cloneable接口。来一个例子,在下例中ComplexDO中包含了SimpleDO对象,要实现ComplexDO深拷贝,则需要先实现SimpleDO的clone接口:

public class SimpleDO implements Cloneable, Serializable {private int x = 1;private String s = "simpleDO";@Overrideprotected Object clone() throws CloneNotSupportedException {SimpleDO newClass = (SimpleDO)super.clone();return newClass;}}public class ComplexDO implements Cloneable, Serializable {private int x = 1;private String s = "complex";private Integer a = 123;private Integer b = 1234;private Integer c = 1334455;private String s2 = "hehehe";private String s3 = "hahahaha";private Long id = 1233245L;private ArrayList<SimpleDO> l = new ArrayList<SimpleDO>();@Overridepublic Object clone() throws CloneNotSupportedException {ComplexDO newClass = (ComplexDO) super.clone();newClass.l = new ArrayList<SimpleDO>();for (SimpleDO simple : this.l) {newClass.l.add((SimpleDO) simple.clone());}return newClass;}}

需要注意的是很多文章说String类型的对象赋值操作符是深拷贝,但是其实在java中使用赋值操作符的都属于浅拷贝,但为什么这么明显的错误这么多的文章会非要说这个是深拷贝呢?我的理解是String、类型的属性都是基本类型,而且提供的方法只要是设计到内部数据的变动都会new一个新的对象出来。所以一个String的操作不会影响到其原先指向的内存。所以一般说String等基础类的赋值操作为深拷贝。

由于这个原因,在使用String字符串拼接的时候,需要开辟新的内存,所以很多人建议用StringBuilder来代替String来做拼接,因为StringBuilder只有在内置的char数组范围不够的时候才重新申请更大的内存(对于现代JVM,会对代码调优,String+String会被优化成StringBuilder.append的相类似的指令)。与拼接相对的裁剪,在String有个subString函数,当使用subString函数时,新String的内部char数组和原String是否相同?这个比较有意思,感兴趣的可以对比看看JDK1.6和JKD1.7的实现。

2. java 序列化实现深拷贝

这种方式的原理是利用java序列化,将一个对象序列化成二进制字节流,然后对该字节流反序列化赋值给一个对象。代码示例:

    public Object seirCopy(Object src) {try {ByteArrayOutputStream byteOut = new ByteArrayOutputStream();ObjectOutputStream out = new ObjectOutputStream(byteOut);out.writeObject(src);ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());ObjectInputStream in = new ObjectInputStream(byteIn);Object dest = in.readObject();return dest;} catch (Exception e) {//do some error handlerreturn null;}}

当然,也可以选用json等序列化的库来完成序列化,这种方式有效的规避了Cloneabel接口的可扩展缺点,一个函数就可以基本上适用于所有的类.缺点是相对内存拷贝,序列化需要先将对象转换成二进制字节流,然后反序列化将该二进制字节流重新拷贝到一块对象内存,相对慢点。

3. 号称最快的深拷贝二方库cloning源码分析

先上源码心得:在cloning中源码中,拷贝的过程主要是靠递归来实现,即对对象内部的每一个属性对象递归调用,直到该属性可直接赋值为止。拷贝的机制是通过反射拿到对象内部的每一个属性,使用Objenesis框架生成该属性的类的新对象,然后赋值。
再看看源码的类图(图中的直线箭头表示调用):

在源码中,核心的处理逻辑在Cloner类中,
分两条递归链路:

  • (1)deepClone->cloneInternal->fastClone->cloneInternal
  • (2)deepClone->cloneInternal->cloneObject->cloneInternal

在(1)中fastClone完成的是继承自IfastCloner接口类的对象,即都是些集合操作的拷贝;
在(2)中cloneObject完成的是通过反射机制拿到普通对象的每一个属性,然后对使用Objenesis新生成对象的属性赋值。

这种方式可扩展性强,不仅可以依靠其现有的代码完成深拷贝,还可以自己定义一些克隆的方式和不需要克隆的类型,灵活性强。
放上源码链接,代码量小,逻辑清晰,适合阅读:https://github.com/kostaskougios/cloning

4. 几种拷贝方式速度的比较

上述3中模式都可以完成深拷贝,那种拷贝的方式速度最快是我们所关心的。
先上测试代码:

    public void testCloneComplex() throws CloneNotSupportedException {final int copyCount = 1;List<ComplexDO> complexDOList = new ArrayList<ComplexDO>(copyCount * 3);final ComplexDO complex = new ComplexDO();//调用二方库long start = System.currentTimeMillis();for(int i = 0; i < copyCount; ++i) {final ComplexDO deepClone = cloner.deepClone(complex);complexDOList.add(deepClone);}long end = System.currentTimeMillis();System.out.println("deepClone cost time=" + (end-start));//调用Cloneable接口实现的clone函数start = System.currentTimeMillis();for(int i = 0; i < copyCount; ++i) {final ComplexDO interfaceClone = (ComplexDO) complex.clone();complexDOList.add(interfaceClone);}end = System.currentTimeMillis();System.out.println("interfaceClone cost time=" + (end-start));//序列化与反序列化生成新对象start = System.currentTimeMillis();for(int i = 0; i < copyCount; ++i) {final ComplexDO seirClone = seirCopy(complex);complexDOList.add(seirClone);}end = System.currentTimeMillis();System.out.println("seirClone cost time=" + (end-start));}
拷贝次数clone接口拷贝序列化拷贝cloning库拷贝
107014
100019826
100041259172

运行结果的单位为毫秒(此数据忽略不计算java热点和可能的gc)。从这个表可以得出结论:

  1. 实现Cloneable接口的拷贝是最快的,因为他只涉及到了内存拷贝,但是如果涉及的属性为普通对象比较多的时候写起来麻烦点
  2. 序列化/反序列化拷贝最慢
  3. 使用cloning库,由于使用了递归和反射机制相对Cloneable接口实现的拷贝要慢,但比序列化方式要快。

这篇关于java 深拷贝探讨的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定