Java对象的拷贝

对象的拷贝

深度拷贝一个对象

java.lang.Object 根类已经定义了 clone() 方法。子类只需要事项 java.lang.Cloneable 接口,就可以返回一个拷贝的对象。实现接口的同时要实现 clone() 方法里面的实现。这是拷贝方式就是浅拷贝(shallow copy of object),这代表着原有对象的值复制一份存储在一个新的对象里面。

浅拷贝并不是完全复制原对象的所有内容,除了基本类型(int,float...),引用类型只是复制一份引用对象的引用,那么原有的对象和拷贝的对象就指共同指向同一个引用对象。这样使用浅拷贝就会导致一些不可预测的错误。看看项目 Exampl1的例子。

public class Example1 {

    public static void main(String[] args) {
        // Make a Vector
        Vector original = new Vector();

        // Make a StringBuffer and add it to the Vector
        StringBuffer text = new StringBuffer("The quick brown fox");
        original.addElement(text);

        // Clone the vector and print out the contents
        Vector clone = (Vector) original.clone();
        System.out.println("A. After cloning");
        printVectorContents(original, "original");
        printVectorContents(clone, "clone");
        System.out.println(
            "--------------------------------------------------------");
        System.out.println();

        // Add another object (an Integer) to the clone and 
        // print out the contents
        clone.addElement(new Integer(5));
        System.out.println("B. After adding an Integer to the clone");
        printVectorContents(original, "original");
        printVectorContents(clone, "clone");
        System.out.println(
            "--------------------------------------------------------");
        System.out.println();

        // Change the StringBuffer contents
        text.append(" jumps over the lazy dog.");
        System.out.println("C. After modifying one of original's elements");
        printVectorContents(original, "original");
        printVectorContents(clone, "clone");
        System.out.println(
            "--------------------------------------------------------");
        System.out.println();
    }

    public static void printVectorContents(Vector v, String name) {
        System.out.println("  Contents of \"" + name + "\":");

        // For each element in the vector, print out the index, the
        // class of the element, and the element itself
        for (int i = 0; i < v.size(); i++) {
            Object element = v.elementAt(i);
            System.out.println("   " + i + " (" + 
                element.getClass().getName() + "): " + 
                element);
        }
        System.out.println();
    }

}

Exampl1 运行输出的结果:

A. After cloning
  Contents of "original":
   0 (java.lang.StringBuffer): The quick brown fox

  Contents of "clone":
   0 (java.lang.StringBuffer): The quick brown fox

--------------------------------------------------------

B. After adding an Integer to the clone
  Contents of "original":
   0 (java.lang.StringBuffer): The quick brown fox

  Contents of "clone":
   0 (java.lang.StringBuffer): The quick brown fox
   1 (java.lang.Integer): 5

--------------------------------------------------------

C. After modifying one of original's elements
  Contents of "original":
   0 (java.lang.StringBuffer): The quick brown fox jumps over the lazy dog.

  Contents of "clone":
   0 (java.lang.StringBuffer): The quick brown fox jumps over the lazy dog.
   1 (java.lang.Integer): 5

--------------------------------------------------------

运行结果打印出3个区域块,在第一个区域块中成功的创建了一个对象和复制拷贝了一个一模一样的对象。从第二个区域块可以看出两个对象是相互对立的,当往复制拷贝的对象添加一个 Integer 数值,打印的结果显示原有的对象不受影响,复制拷贝的对象打印添加的数值。第三个区域块修改了 StringBuffer 的值,打印显示出两个对象里面的内容同时被修改了,这恰恰验证了上面的说法,应用类型的值时用 clone 时无法被完全复制过去的。

所以使用 clone 这种方法复制内容可能会造成不可预测的错误。

为了解决浅拷贝带来的问题,另外的解决方案是使用深度拷贝(deep copy of object) 。深度拷贝使得原有对象和新对象完全独立,对象里面的所有属性都完成被复制过去。Java Api 提供的 clone 方法无法一步到位的深度拷贝,只能为对象里面的所有的熟悉均实现 Cloneable 接口,为所有的属性变量 clone 一遍。就如同上的例子,需要重写 clone 方法拷贝一个新的字符串,再把新的字符串对象复制给 Vector克隆出来的对象。
不过这种做法有几点缺点:

  1. 要拷贝一个对象,必须修改这个类里面所有元素的 clone 方法。假如你使用第三库的代码,而且没有源码这就无法使用这个方法,还有如果使用 final 修饰变量这也是没使用这个拷贝方法
  2. 要有权限访问父类属性变量,如果某个变量是私有属性就没有权限修改它
  3. 必须要确定属性变量的类型,尤其不能运行中才能确定的类型
  4. 操作繁琐容易出错

另外一种通用的办法就是使用 Java Object Serialization (JOS) ,使用序列化,使用把 ObjectOutputStream 对象写入出入到一个数组中,再使用 ObjectInputStream 复制到另一个对象,从而得到两个完全独立的对象。

import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;

/**
 * Utility for making deep copies (vs. clone()'s shallow copies) of 
 * objects. Objects are first serialized and then deserialized. Error
 * checking is fairly minimal in this implementation. If an object is
 * encountered that cannot be serialized (or that references an object
 * that cannot be serialized) an error is printed to System.err and
 * null is returned. Depending on your specific application, it might
 * make more sense to have copy(...) re-throw the exception.
 *
 * A later version of this class includes some minor optimizations.
 */
public class UnoptimizedDeepCopy {

    /**
     * Returns a copy of the object, or null if the object cannot
     * be serialized.
     */
    public static Object copy(Object orig) {
        Object obj = null;
        try {
            // Write the object out to a byte array
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(orig);
            out.flush();
            out.close();

            // Make an input stream from the byte array and read
            // a copy of the object back in.
            ObjectInputStream in = new ObjectInputStream(
                new ByteArrayInputStream(bos.toByteArray()));
            obj = in.readObject();
        }
        catch(IOException e) {
            e.printStackTrace();
        }-快速深度拷贝一个对象
        catch(ClassNotFoundException cnfe) {
            cnfe.printStackTrace();
        }
        return obj;
    }

}


这种方法也是有缺点的:

  1. 必须序列化,都要实现 java.io.Serializable 接口
  2. 序列化影响程序运输速度。
  3. 可以适应不同的对象大小,创建适合大小的数组,可以在多线程任务中安全使用。符合上面种种特点,可想而知速度是有多慢。

出于以上的种种缺点,可以对 ByteArrayOutputStream 和 ByteArrayInputStream 做出3点优化:

  1. ByteArrayOutputStream 默认初始化 32 byte的数组,遇到大的对象就增大为目前两倍大的数组,这意味着要摒弃到初始化的小数组,优化这个问题的方法也简单,初始化一个大的数组。
  2. ByteArrayOutputStream 修改的内容都是 synchronized 修饰的,这是个安全的做法,但是在确定的单一线程里许可以抛弃 synchronized 这样做也是安全的。
  3. toByteArray() 方法会创建和复制一个字节流数组。检索数据数组然后继续写入流中,原有的数据就不会被改变。尽管如此,这循环复制一个数组会造成多余的垃圾回收工作。

深度拷贝速度优化

FastByteArrayOutputStream.class

import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayInputStream;

/**
 * ByteArrayOutputStream implementation that doesn't synchronize methods
 * and doesn't copy the data on toByteArray().
 */
public class FastByteArrayOutputStream extends OutputStream {
    /**
     * Buffer and size
     */
    protected byte[] buf = null;
    protected int size = 0;

    /**
     * Constructs a stream with buffer capacity size 5K 
     */
    public FastByteArrayOutputStream() {
        this(5 * 1024);
    }

    /**
     * Constructs a stream with the given initial size
     */
    public FastByteArrayOutputStream(int initSize) {
        this.size = 0;
        this.buf = new byte[initSize];
    }

    /**
     * Ensures that we have a large enough buffer for the given size.
     */
    private void verifyBufferSize(int sz) {
        if (sz > buf.length) {
            byte[] old = buf;
            buf = new byte[Math.max(sz, 2 * buf.length )];
            System.arraycopy(old, 0, buf, 0, old.length);
            old = null;
        }
    }

    public int getSize() {
        return size;
    }

    /**
     * Returns the byte array containing the written data. Note that this
     * array will almost always be larger than the amount of data actually
     * written.
     */
    public byte[] getByteArray() {
        return buf;
    }

    public final void write(byte b[]) {
        verifyBufferSize(size + b.length);
        System.arraycopy(b, 0, buf, size, b.length);
        size += b.length;
    }

    public final void write(byte b[], int off, int len) {
        verifyBufferSize(size + len);
        System.arraycopy(b, off, buf, size, len);
        size += len;
    }

    public final void write(int b) {
        verifyBufferSize(size + 1);
        buf[size++] = (byte) b;
    }

    public void reset() {
        size = 0;
    }

    /**
     * Returns a ByteArrayInputStream for reading back the written data
     */
    public InputStream getInputStream() {
        return new FastByteArrayInputStream(buf, size);
    }

}

ByteArrayInputStream.class

import java.io.InputStream;
import java.io.IOException;

/**
 * ByteArrayInputStream implementation that does not synchronize methods.
 */
public class FastByteArrayInputStream extends InputStream {
    /**
     * Our byte buffer
     */
    protected byte[] buf = null;

    /**
     * Number of bytes that we can read from the buffer
     */
    protected int count = 0;

    /**
     * Number of bytes that have been read from the buffer
     */
    protected int pos = 0;

    public FastByteArrayInputStream(byte[] buf, int count) {
        this.buf = buf;
        this.count = count;
    }

    public final int available() {
        return count - pos;
    }

    public final int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }

    public final int read(byte[] b, int off, int len) {
        if (pos >= count)
            return -1;

        if ((pos + len) > count)
            len = (count - pos);

        System.arraycopy(buf, pos, b, off, len);
        pos += len;
        return len;
    }

    public final long skip(long n) {
        if ((pos + n) > count)
            n = count - pos;
        if (n < 0)
            return 0;
        pos += n;
        return n;
    }

}

ByteArrayInputStream 和 ByteArrayOutputStream 的使用

import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;

/**
 * Utility for making deep copies (vs. clone()'s shallow copies) of 
 * objects. Objects are first serialized and then deserialized. Error
 * checking is fairly minimal in this implementation. If an object is
 * encountered that cannot be serialized (or that references an object
 * that cannot be serialized) an error is printed to System.err and
 * null is returned. Depending on your specific application, it might
 * make more sense to have copy(...) re-throw the exception.
 */
public class DeepCopy {

    /**
     * Returns a copy of the object, or null if the object cannot
     * be serialized.
     */
    public static Object copy(Object orig) {
        Object obj = null;
        try {
            // Write the object out to a byte array
            FastByteArrayOutputStream fbos = 
                    new FastByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(fbos);
            out.writeObject(orig);
            out.flush();
            out.close();

            // Retrieve an input stream from the byte array and read
            // a copy of the object back in. 
            ObjectInputStream in = 
                new ObjectInputStream(fbos.getInputStream());
            obj = in.readObject();
        }
        catch(IOException e) {
            e.printStackTrace();
        }
        catch(ClassNotFoundException cnfe) {
            cnfe.printStackTrace();
        }
        return obj;
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容

  • Java对象的Copy 引用CSDN博客地址:http://m.blog.csdn.net/chenssy/art...
    佩佩691阅读 901评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,547评论 18 399
  • 气温的不断下降,全国各地也都开始飘雪,但是我们处在这个不南不北的城市,却始终平静,有些来自南方的同学迫不及待的想要...
    尋找阿水阅读 236评论 1 0
  • 自从看了《朗读者》之后就感慨很多 尤其是陆川在读了一篇“藏羚羊的跪拜” 万物皆是生灵 大自然赋予我们能力 就尽量不...
    我是一只爱发呆的懒猫阅读 232评论 0 0
  • 有些人,似假茶,不必在意 有些人,如新茶,可以回味 有些人,像熟茶,可以暖心 有些人,是老茶,值得感悟 心放下了,...
    云上文化阅读 292评论 0 0