Java对象克隆

引自:Java对象克隆(Clone)及Cloneable接口、Serializable接口的深入探讨
https://blog.csdn.net/kenthong/article/details/5758884

为了更好地解读,此文进行了再编辑,重新排版,并结合自己的理解做了一些修改,方便大家交流。

问题背景

谈到了对象的克隆,就不得不说为什么要对对象进行克隆。Java中所有的对象都是保存在堆中,而堆是供全局共享的。也就是说,如果同一个Java程序的不同方法,只要能拿到某个对象的引用,引用者就可以随意的修改对象的内部数据(前提是这个对象的内部数据通过get/set方法曝露出来)。有的时候,我们编写的代码想让调用者只获得该对象的一个拷贝(也就是一个内容完全相同的对象,但是在内存中存在两个这样的对象),有什么办法可以做到呢?当然是克隆咯。

举例说明

下面我们创建一个implements了java.lang.Cloneable接口的类User,就是说User这个类可以被克隆。

import java.util.Date;
public class User implements Cloneable {
    private String username;
    private String password;
    private Date birthdate;
    public User(String username, String password, Date birthdate) {
        this.username = username;
        this.password = password;
        this.birthdate = birthdate;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public int hashCode() {
        // 省略equals的实现(可用eclipse自动生成)
    }
    @Override
    public boolean equals(Object obj) {
        // 省略equals的实现(可用eclipse自动生成)
    }
    // 省略一大堆get/set方法
}

不懂Cloneable是什么,我们就回去点击查看这个接口的定义,然后我们发现,并没有什么,连一个方法都没有:

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

我们可以看到虽然没有定义什么方法,但是Cloneable接口的注释里面前几行写了:

/*A class implements the <code>Cloneable</code> interface 
to indicate to the {@link java.lang.Object#clone()} method 
that it is legal for that method to make a field-for-field copy 
of instances of that class.*/

也就是它明确说明: 一个实现了Cloneable接口的类会指示:该类可以使用Object中的clone()方法来实现实例的复制。由此我们获得明确指示,可以去探寻 Object 类中的 clone()方法:

     /* @return     a clone of this instance.
     * @throws  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    protected native Object clone() throws CloneNotSupportedException;

没错,又是个native方法,果然是个高深的东西,不过我们还是要占一下他的便宜。这个方法是protected的,分明就是叫我们去占便宜的。

再继续看看下面测试代码。

这里默认大家明白: == 和 equals() 的区别
== : 完全相同(内容+引用)就是指向的是同一个东西,比如一个人有俩名字A和B,不管叫谁,说的都是他。
equals():相同的内容,不同的地址(引用不同) 就是相当于两个长得几乎一毛一样没有差异的的双胞胎,除了名字不一样都一样,但是他们毕竟不是同一个。

import java.util.Date;
import org.junit.Test;
public class TestCase {
    
    @Test
    public void testUserClone() throws CloneNotSupportedException {
        User u1 = new User("Kent", "123456", new Date());
        User u2 = u1;
        User u3 = (User) u1.clone();
        
        System.out.println(u1 == u2);       // true
        System.out.println(u1.equals(u2));  // true
        
        System.out.println(u1 == u3);       // false
        System.out.println(u1.equals(u3));  // true
    }
}

这个clone()方法果然牛,一下子就把我们的对象克隆了一份,执行结果也符合我们的预期,u1和u3的地址不同但是内容相同。

通过上述的例子,我们可以看出,要让一个对象进行克隆,其实就是两个步骤:

  1. 让该类实现java.lang.Cloneable接口;
  2. 重写(override)Object类的clone()方法。

但是,事实上真的是如此简单吗?再看下面的代码。

public class Administrator implements Cloneable {
    private User user;
    private Boolean editable;
    public Administrator(User user, Boolean editable) {
        this.user = user;
        this.editable = editable;
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public int hashCode() {
        // 老规矩
    }
    @Override
    public boolean equals(Object obj) {
        // 老规矩
    }
    // 老规矩
}

上面定义了一个Administrator类,这个类持有一个User类的对象。接下来我们看看对Administrator对象进行克隆会有什么效果。

import java.util.Date;
import org.junit.Test;
public class TestCase {
    @Test
    public void testAdministratorClone() throws CloneNotSupportedException {
        Administrator a1 = new Administrator(new User("Kent", "123456", new Date()), true);
        Administrator a2 = a1;
        Administrator a3 = (Administrator) a1.clone();
        
        System.out.println(a1 == a2);           // true
        System.out.println(a1.equals(a2));      // true
        
        System.out.println(a1 == a3);           // false
        System.out.println(a1.equals(a3));      // true
        
        System.out.println(a1.getUser() == a3.getUser());  //true! It's not what we expected!!!!!
        System.out.println(a1.getUser().equals(a3.getUser()));  //true
    }
}

呵呵呵!出问题了吧。Java哪是那么容易就能驾驭的说!

这里我们就可以引入两个专业的术语:浅克隆(shallow clone)和深克隆(deep clone)。

浅克隆 & 深克隆

所谓的浅克隆,顾名思义就是很表面的很表层的克隆,如果我们要克隆Administrator对象,只克隆他自身以及他包含的所有对象的引用地址。

而深克隆,就是非浅克隆、深层次的克隆。克隆除自身以外所有的对象,包括自身所包含的所有对象实例。至于深克隆的层次,由具体的需求决定,也有“N层克隆”一说。

但是,所有的基本(primitive)类型数据,无论是浅克隆还是深克隆,都会进行原值克隆。毕竟他们都不是对象,不是存储在堆中。注意:基本数据类型并不包括他们对应的包装类。

如果我们想让对象进行深度克隆,我们可以这样修改Administrator类。

@Override
protected Object clone() throws CloneNotSupportedException {
    Administrator admin = (Administrator) super.clone();
    admin.user = (User) admin.user.clone(); //层层克隆
    return admin;
}

由于Boolean会对值进行缓存处理,所以我们没必要对Boolean的对象进行克隆。并且Boolean类也没有实现java.lang.Cloneable接口。

上面的深度克隆方法总结一下就是:

  1. 让该类实现java.lang.Cloneable接口;
  2. 确认持有的对象是否实现java.lang.Cloneable接口并提供clone()方法;
  3. 重写(override)Object类的clone()方法,并且在方法内部调用持有对象的clone()方法;
  4. ……
  5. 多麻烦啊,调来调去的,如果有N多个持有的对象,那就要写N多的方法,突然改变了类的结构,还要重新修改clone()方法。

难道就没有更好的办法吗?
答案是:我们可以考虑使用java.lang.Serializable来实现对象的深度克隆。

用 java.lang.Serializable来实现对象的深度克隆。

首先,看一下Serializable是什么。
从名字我们知道,这是序列化的工具,序列化通俗的讲,是讲一个Object转化为更为底层的字节序列。以对其进行存储和传输等。详细介绍参见另一个文章《Java 序列化Serializable详解(附详细例子)》
https://blog.csdn.net/sheepmu/article/details/27579895
我们可以看一下Serializable接口的定义(注释太长,我节选最有用的几行贴出来):

/*
 * @author  unascribed
 * @see java.io.ObjectOutputStream
 * @see java.io.ObjectInputStream
 * @see java.io.ObjectOutput
 * @see java.io.ObjectInput
 * @see java.io.Externalizable
 * @since   JDK1.1
 */
public interface Serializable {
}

从中我们可以看到和Cloneable·定义中类似的情况: 只要实现了Serializable 接口的类,都可以使用java.io.ObjectOutputStream java.io.ObjectInputStream java.io.ObjectOutput java.io.ObjectInput java.io.Externalizable

明白了Serializable是啥以后,我们将编写一个工具类并提供cloneTo()方法。这个工具类里面其实用的就是Serializable涉及到的那几个Object类中的方法,也就是真正实现 Serializable 功能的方法:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public abstract class BeanUtil {
    @SuppressWarnings("unchecked")
    public static  T cloneTo(T src) throws RuntimeException {
        ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        T dist = null;
        try {
            out = new ObjectOutputStream(memoryBuffer);
            out.writeObject(src);
            out.flush();
            in = new ObjectInputStream(new ByteArrayInputStream(memoryBuffer.toByteArray()));
            dist = (T) in.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (out != null)
                try {
                    out.close();
                    out = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            if (in != null)
                try {
                    in.close();
                    in = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
        }
        return dist;
    }
}

看不懂,没关系,直接拿去用就可以了。嘻嘻。

接下来我们测试一下是否能通过这个工具来实现深度克隆。

import java.util.Date;
import org.junit.Test;
public class TestCase {
    
    @Test
    public void testCloneTo() {
        Administrator src = new Administrator(new User("Kent", "123456", new Date()), true);
        Administrator dist = BeanUtil.cloneTo(src);
        
        System.out.println(src == dist);            // false
        System.out.println(src.equals(dist));       // true
        
        System.out.println(src.getUser() == dist.getUser());        //false ! Well done!
        System.out.println(src.getUser().equals(dist.getUser()));   //true
    }
}

好了,无论你的对象有多么的复杂,只要这些对象都能够实现java.lang.Serializable接口,就可以进行克隆,而且这种克隆的机制是JVM完成的,不需要修改实体类的代码,方便多了。

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

推荐阅读更多精彩内容