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";

        @Override
        protected 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>();

        @Override
        public 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 handler
            return 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库拷贝
1 0 70 14
100 0 198 26
1000 4 1259 172

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

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

java 深拷贝探讨的相关文章

探讨java深拷贝_java

本文将讨论以下4个问题     1. java Cloneable接口实现深拷贝     2. java 序列化实现深拷贝     3. 号称最快的深拷贝二方库cloning源码分析     4. 几种拷贝方式速度的比较 深拷贝的概念本文就不说了.在C++中实现深拷贝一般情况下重载赋值操作符 "=" 来实现同一个类的对象间的深拷贝,所以很自然的在java中我们也同样可以定义一个copy函数,在函数内部为对象的每一个属性作赋值操作.这种方式简单自然,但存在一个致命性的问题:如果有一天在类

Java 深拷贝与浅拷贝的分析_java

在正式的进入主题之前,我们先来了解下深拷贝和前拷贝的概念: 浅拷贝: 会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,如果属性是基本类型,拷贝的是基本类型的值:如果属性是内存地址,拷贝的就是内存地址,因此如果一个对象改变了这个地址就会影响到另一个对象: 深拷贝: 不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值: 了解完概念之后,我们来测试下普通的对象赋值操作属于深拷贝还是浅拷贝: 测试代码: public class Depth

一种c#深拷贝方式完胜java深拷贝(实现上的对比分析)_C#教程

楼主是一名asp.net攻城狮,最近经常跑java组客串帮忙开发,所以最近对java的一些基础知识特别上心.却遇到需要将一个对象深拷贝出来做其他事情,而原对象保持原有状态的情况.(实在是不想自己new一个出来,然后对着一堆字段赋值......好吧,再此之前我没有关心是否项目框架有深拷贝的方法),然后就想着用反射实现吧....接下来 是我自己的原因,还是真的不存在这样的纯用反射实现的深拷贝方式....(c#是有纯反射实现的) 但也不能算自己白忙活吧,也找到了其他实现深拷贝的方式(但是每种方式我都觉

阿里技术协会(ATA)11月系列精选文集

JAVA核心技术 1.面向GC的Java编程 2.JVM飙高排查脚本-结构分析 3.理解Java NIO 4.杜绝假死,Tomcat容器做到自我保护,设置最大连接数 5.Groovy与Java集成常见的坑 6.java 深拷贝探讨 分布式计算 1.jstorm 介绍 2.Spark的调度策略详解 3.生活中的Paxos,原来你我都在使用--对Paxos生活化的解读(一) 4.生活中的Paxos,原来你我都在使用--对Paxos生活化的解读(二) 5.消息中间件MetaQ高性能原因分析 大数据 1

Java中的深拷贝(深复制)和浅拷贝(浅复制)介绍_java

深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java.虽然java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑. 浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象.深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象.举例来说更加清楚:对象A1中包含对B1的引用

Java实现文件拷贝的测试

经过一组简单的测试发现JAVA NIO提供的文件内存映射方法实现文件拷贝速度最快,不管是大文件还是小文件,特别是大文件的拷贝速度比普通方法提高20倍,唯一有个前提就是内存需要足够大,否则文件映射肯定失败(当然可以通过分割文件,部分映射的方法避免,但就比较麻烦了):其次NIO提供的文件管道传输速度也比较好,如果没法做文件内存映射,推荐这种拷贝方法:另外,Buffer的大小,对于读写速度还是有影响的,基本就是Buffer越大读写越快(有个疑问就是Buffer.allocateDirec()效率提高不

探讨Java与Ruby语言迁移时的安全性

在这篇文章中,我们将要探讨Java与Ruby语言迁移时风险预测方面的问题. 通常来说,"使用Ruby具有风险"是一种普遍的看法,这存在一定的原因.因为使用新的语言天生是有风险的.随着Ruby on Rails逐步进入到主流的开发领域中,这样的风险将会随时间逐渐降低,因为有逐步增长的开发者群.组件(或称作gems和plug-ins)相关的书籍.以及业务合作伙伴与你沟通交流.但同时你也可以听到主流的观点指出"使用Java是安全的".对于这种的观点,我持有强烈的反对意见.

探讨Java内部类的可见性

在Java中,当生成一个内部类的对象时,此对象与制造它的外部类通过外部类的.this保持着联系,因此该内部类对象可以访问其外部类对象的所有成员,包括private成员. 而该内部类对象对于其他类的对象的访问,遵照常规的访问权限语法,这一点也没有什么特别支持.这里需要探讨的是,外部类以及其他类的对象可以如何访问到某个内部类对象,即内部类的可见性问题. 下面是一个示例程序Out.java,其中包含了4个不同访问权限的内部类(private,default,protected,public),在每个内

浅析Java中的深拷贝与浅拷贝

原文:http://java.chinaitlab.com/oop/784559.html  首先我们看看浅拷贝和深拷贝的定义     浅拷贝:只复制一个对象,对象内部存在的指向其他对象数组或者引用则不复制     深拷贝:对象,对象内部的引用均复制     为了更好的理解它们的区别我们假设有一个对象A,它包含有2对象对象A1和对象A2       对象A进行浅拷贝后,得到对象B但是对象A1和A2并没有被拷贝     对象A进行深拷贝,得到对象B的同时A1和A2连同它们的引用也被拷贝     在