美文网首页
Java对象克隆

Java对象克隆

作者: Reinelili | 来源:发表于2020-03-07 21:45 被阅读0次

引自: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完成的,不需要修改实体类的代码,方便多了。

相关文章

  • 使用对象序列化和反序列化实现深度克隆

    java对象进行克隆,分为浅克隆和深克隆 浅克隆需要实现java.lang.Cloneable接口,并重写java...

  • java克隆

    java克隆 java克隆分为浅克隆和深克隆,概念如下: 浅拷贝(浅克隆)克隆出来的对象的所有变量都含有与原来的对...

  • 理解Java浅克隆和深克隆

    克隆概念 Java一切皆对象,克隆就是对对象的克隆;克隆可能听起来有点高级,也可以为对象复制或者对象拷贝。平时开发...

  • Java深复制浅复制解析.md

    Java 克隆概念 Java克隆分为深克隆和浅克隆两种类型。 浅复制(浅克隆)被复制对象的所有变量都含有与原来的对...

  • Java | 对象克隆

    浅拷贝 默认的克隆操作,也就是Object的 clone 方法是浅拷贝,浅拷贝只会拷贝对象属性的值,而不会拷贝对象...

  • Java对象克隆

    在Java中数据类型分为值(基本数据类型)类型和引用类型。深克隆和浅克隆的重要区别在于是否支持引用类型(数组、类、...

  • Java对象克隆

    引自:Java对象克隆(Clone)及Cloneable接口、Serializable接口的深入探讨https:/...

  • Java 对象克隆

    所谓的对象克隆描述的概念就是进行对象的赋值,当一个对象创建完成之后实际上都会自动的开辟内存空间,在每一块堆内存空间...

  • java 克隆对象

    P:java分基本数据类型和引用数据类型。 (1)假克隆: 对于 = 等于号的克隆形式, 基本数据类型是值拷贝.相...

  • Java 对象克隆

    Java的拷贝可以分为浅拷贝和深拷贝。 浅拷贝 原变量和拷贝变量引用同一个对象,改变一个变量锁引用的对象将会对另一...

网友评论

      本文标题:Java对象克隆

      本文链接:https://www.haomeiwen.com/subject/wxujqctx.html