小言_互联网的博客

一文了解Java对象的克隆,深浅拷贝(克隆)

444人阅读  评论(0)

一、什么是对象的克隆?

在Java的Object类中,有一个方法名为clone(),直译过来就是克隆,核心概念就是复制对象并返回一个新的对象。

protected native Object clone() throws CloneNotSupportedException;

官方对该方法的描述翻译过来是这样的:

创建并返回该对象的副本。“复制”准确的含义可能取决于对象的类。一般的含义是指:对于任意对象X,表达式 x.clone() != x 的布尔结果为true,并且x.clone().getClass() ==x.getClass()的布尔结果也是true。但是这往往不是绝对的要求。通常情况是:x.clone().equals(x)的表达式结果为true,这不是一个绝对的要求。
按照惯例,返回的对象应该通过调用super.clone获取。如果一个类和他的所有超类(Object除外)都遵守这个惯例,那么就会是x.clone().getClass() == x.getClass()的情况。 按照惯例,此方法返回的对象应该与当前对象(被克隆的对象)是独立无关的。为了实现这个独立性,可能需要修改super.clone返回的对象的一个或者多个字段。这就意味着复制任何可变对象的时候,需要包含被克隆对象的内部“深层结构”,并替换对象副本中的对象的引用。如果一个类仅包含原始字段类型或者是不可变对象的引用,则通常情况下,super.clone()方法返回的对象中的字段是不需要修改的。
Object的clone()方法执行特定的克隆操作。首先,如果当前对象的类没有实现Cloneable接口,那么就会抛出一个CloneNotSupportException的异常。注意,所有数组都被认为是实现了Cloneable接口,并且数组T[]的clone方法的返回类型是T[], 其中T是任何引用或者原始数据类型。 换句话说就是,这个方法将会创建一个当前对象类的一个新实例,并将所有字段初始化为与当前对象的对应字段的值一致的内容,这个过程就像是通过赋值一样。这些字段的内容本身不被克隆。因此,这个方法表现的是一种“浅拷贝”,而不是“深度拷贝”操作。
Object类本身并不实现Cloneable接口,因为在类别为Object的对象上调用clone方法将导致运行时抛出异常。

 

二、如何进行对象克隆?

(1)在要实现克隆的对象类中实现Cloneable接口。
为啥?Cloneable接口为标记接口(标记接口为用户标记实现该接口的类具有该接口标记的功能,常见的标记接口有Serializable、Cloneable、RandomAccess),如果没有实现该接口,在调用clone方法时就会抛出CloneNotSupportException异常。

(2)在类中重写Object的clone方法。
为啥?重写是为了扩大访问权限,如果不重写,因Object的clone方法的修饰符是protected,除了与Object同包(java.lang)和直接子类能访问,其他类无权访问。并且默认Object的clone表现出来的是浅拷贝,如果要实现深拷贝,也是需要重写该方法的。

 

三、测试(浅克隆)

按照前面官方文档提到的,clone通常是一个浅拷贝,如果要做到深拷贝,需要对复制对象中的对象引用进行修改,换句话说就是浅拷贝的效果就是引用例行的属性无法完全复制,被克隆对象和克隆对象中的该引用类型的属性指向同一个引用,并不是完全独立无关的
举例:
创建一个User类,其中包含一个引用类型的属性cp:

public class User implements Cloneable{
    private String name;
    private double height;
    private double length;
    private int gender;
    private Couple cp;

    // 省略构造方法,getter,setter和toString()
    @Override
    protected User clone() throws CloneNotSupportedException {
        return (User) super.clone();
    }
}

创建Couple类

public class Couple {
    private String name;
    private int gender;
// 省略构造方法和getter,setter和toString()
}

编写测试:

public static void main(String[] args) throws CloneNotSupportedException {
    Couple cp = new Couple("张小小",0);
    User user = new User("老王",180.0,18.0, 1, cp);
    System.out.println("克隆之前的老王===================");
    System.out.println(user);

    // 克隆老王
    User newUser =  user.clone();
    System.out.println("克隆出来的新老王=================");
    System.out.println(newUser);

    // 改变老王cp信息的值及个人信息
    user.setName("老王爱花姑娘");
    user.setGender(0);
    cp.setGender(1);

    System.out.println("发生变化之后的旧老王================");
    System.out.println(user);
    System.out.println("发生变化之后的新老王================");
    System.out.println(newUser);
}

测试结果:

浅拷贝的情况下,原被克隆对象发生变化后,克隆对象的基本数据类型和不可变引用数据类型(String)的数据未发生影响,而cp字段为可变的应用类型,可以观察到克隆对象的内容随着被克隆对象的变化发生了同样的变化,说明两个对象的cp属性字段可能指向同一个引用,才会造成这样的结局。

 

四、深拷贝(深克隆)

以上章节中的浅拷贝的效果往往达不到我们的要求,因为在实际使用时,我们肯定是希望新拷贝出来的对象不受原对象的影响,否则咱们做出拷贝的意义何在?(我就碰到过因为对象被同事插进来的代码导致对象发生了变更,代码出现BUG的问题,后面是使用的深拷贝才消除同事的代码对该对象的影响)那么如何实现对象的深拷贝呢?列出以下几种常见的方式:

(1)clone函数的嵌套调用

既然引用类型无法被完全克隆,那么我们可以考虑在引用类型所在的类也实现Cloneable接口,在外层User类的clone方法调用属性的克隆方法。
Couple类实现Cloneable接口,重写Object的clone方法。

public class Couple implements Cloneable{
    private String name;
    private int gender;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

User类修改clone方法,在clone方法中调用Couple类的clone方法。

@Override
protected User clone() throws CloneNotSupportedException {
    User user = (User) super.clone();
    user.cp = (Couple) this.cp.clone();
    return user;
}

同样的测试,查看测试结果:

以上我们看到已经达到深度拷贝的效果了,但是这种嵌套调用clone()方法存在问题:

  • 如果有属性是数组类型呢?
    官方文档明确说明虽然针对所有数组类型都认为是已经实现了Cloneable接口(见第一节),但是实际克隆的时候可能仍然表现出浅拷贝。如果这一点不注意,在重写clone方法嵌套调用时未能正确调用clone,依然会出现浅拷贝的问题。

 
我们在User类中加一个Couple[]数组cps, 然后按照要求在User类的clone方法中嵌套调用一下数组Couple[] 的clone方法。

public class User implements Cloneable{
    private String name;
    private double height;
    private double length;
    private int gender;
    private Couple cp;
    private Couple[] cps;

    @Override
    protected User clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.cp = (Couple) this.cp.clone();
        user.cps = this.cps.clone();
        return user;
    }
}

user.cps = this.cps.clone();调用似乎与上面一行一样,没什么问题,但是我们针对这个数组类型做一个测试看看结果,修改测试代码:

public static void main(String[] args) throws CloneNotSupportedException {
    Couple cp = new Couple("张小小",0);
    Couple cp2 = new Couple("李小姐",0);
    User user = new User("老王",180.0,18.0, 1, cp);
    // 给user添加一个cps的数组类型
    Couple[] cps = new Couple[3];
    cps[0]=cp;
    cps[1]=cp2;
    user.setCps(cps);

    System.out.println("克隆之前的老王===================");
    System.out.println(user);

    // 克隆老王
    User newUser =  user.clone();
    System.out.println("克隆出来的新老王=================");
    System.out.println(newUser);

    // 改变老王cp信息的值及个人信息
    user.setName("老王爱花姑娘");
    user.setGender(0);
    cp.setGender(1);
    // 让数组类型cps也发生变化,放入一个新值
    Couple cp3 = new Couple("柳一一",0);
    cps[2]=cp3;

    System.out.println("发生变化之后的旧老王================");
    System.out.println(user);
    System.out.println("发生变化之后的新老王================");
    System.out.println(newUser);
}

观察运行结果:

克隆之前的老王===================
User{name='老王', height=180.0, length=18.0, gender=1, cp=Couple{name='张小小', gender=0}, cps=[Couple{name='张小小', gender=0}, Couple{name='李小姐', gender=0}, null]}
克隆出来的新老王=================
User{name='老王', height=180.0, length=18.0, gender=1, cp=Couple{name='张小小', gender=0}, cps=[Couple{name='张小小', gender=0}, Couple{name='李小姐', gender=0}, null]}
发生变化之后的旧老王================
User{name='老王爱花姑娘', height=180.0, length=18.0, gender=0, cp=Couple{name='张小小', gender=1}, cps=[Couple{name='张小小', gender=1}, Couple{name='李小姐', gender=0}, Couple{name='柳一一', gender=0}]}
发生变化之后的新老王================
User{name='老王', height=180.0, length=18.0, gender=1, cp=Couple{name='张小小', gender=0}, cps=[Couple{name='张小小', gender=1}, Couple{name='李小姐', gender=0}, null]}

Process finished with exit code 0

观察测试结果,我们会发现两个对象的cps的第3个元素不一样,说明Couple[]数组是深拷贝的,这两个数组的引用指向不同的地方,但是奇怪的是克隆对象的第1个元素随着被克隆对象的第1个元素发生了变化,表现出来浅拷贝的现象。这是因为虽然Couple[]数组做出了引用修改,拷贝数组和原数组指向两个不同的引用,但是里面的元素存储的仍然是对象的引用地址,所以当第1个元素发生变化时,由于两个对象数组的第1个元素都指向同一个引用,所以都会受到影响,从而表现出浅拷贝的结果。
我们针对这个问题对User的clone方法再次修改如下:

    @Override
    protected User clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.cp = (Couple) this.cp.clone();
//        user.cps = this.cps.clone();
        Couple[] newCps = new Couple[this.cps.length];
        for (int i=0; i<this.cps.length;i++){
            if (this.cps[i] == null){
                continue;
            }
            Couple cp = (Couple) this.cps[i].clone();
            newCps[i] = cp;
        }
        user.cps = newCps;
        return user;
    }

这样我们再观察结果就会发现克隆对象和被克隆对象完全独立无关了,被克隆对象发生变化,克隆对象不会发生任何问题。
通过上面的操作,我们会发现这种嵌套调用clone方法存在两个弊端

  1. 每次都需要手写clone方法,而且需要给其中嵌套的每一个类都重写clone方法,步骤相当繁琐。
  2. 如果出现数组类型的属性,那么重写clone调用时需要相当小心,一不小心可能就会出现浅拷贝的问题。有的人会说还好吧,但是假如这个数组是个二维数组呢?三维数组呢
     

(2)对象的序列化和反序列化

我们也可以通过对象的序列化和反序列化来实现对象的深拷贝。对象的序列化和反序列化的手段有很多,比如:

  • IO 流
  • Json、XML这种
  • Builder建造,例如Lombok实现的@Builder,但是lombok的Builder在面对数组类型的属性时也容易出现浅拷贝的问题(类似于嵌套clone),具体可自行测试.

下面我们以最经典的IO流来举例:
我们将User类和Couple类都实现Serializable接口。不实现该接口会抛出java.io.NotSerializableException异常。然后修改我们的测试代码:
将原来调用的clone()方法改为输入输出流

public static void main(String[] args) throws IOException, ClassNotFoundException {
    Couple cp = new Couple("张小小",0);
    Couple cp2 = new Couple("李小姐",0);
    User user = new User("老王",180.0,18.0, 1, cp);
    // 给user添加一个cps的数组类型
    Couple[] cps = new Couple[3];
    cps[0]=cp;
    cps[1]=cp2;
    user.setCps(cps);

    System.out.println("克隆之前的老王===================");
    System.out.println(user);

    // 克隆老王
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    ObjectOutputStream oo = new ObjectOutputStream(bout);
    oo.writeObject(user);// 将对象序列化
    // 读取序列化后的数据到输入流中
    ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
    ObjectInputStream oi = new ObjectInputStream(bin);
    User newUser = (User) oi.readObject();


    System.out.println("克隆出来的新老王=================");
    System.out.println(newUser);

    // 改变老王cp信息的值及个人信息
    user.setName("老王爱花姑娘");
    user.setGender(0);
    cp.setGender(1);
    // 让数组类型cps也发生变化,放入一个新值
    Couple cp3 = new Couple("柳一一",0);
    cps[2]=cp3;

    System.out.println("发生变化之后的旧老王================");
    System.out.println(user);
    System.out.println("发生变化之后的新老王================");
    System.out.println(newUser);
}

测试结果为正常深拷贝。

附Java doc原文:

/**
 * Creates and returns a copy of this object.  The precise meaning
 * of "copy" may depend on the class of the object. The general
 * intent is that, for any object {@code x}, the expression:
 * <blockquote>
 * <pre>
 * x.clone() != x</pre></blockquote>
 * will be true, and that the expression:
 * <blockquote>
 * <pre>
 * x.clone().getClass() == x.getClass()</pre></blockquote>
 * will be {@code true}, but these are not absolute requirements.
 * While it is typically the case that:
 * <blockquote>
 * <pre>
 * x.clone().equals(x)</pre></blockquote>
 * will be {@code true}, this is not an absolute requirement.
 * <p>
 * By convention, the returned object should be obtained by calling
 * {@code super.clone}.  If a class and all of its superclasses (except
 * {@code Object}) obey this convention, it will be the case that
 * {@code x.clone().getClass() == x.getClass()}.
 * <p>
 * By convention, the object returned by this method should be independent
 * of this object (which is being cloned).  To achieve this independence,
 * it may be necessary to modify one or more fields of the object returned
 * by {@code super.clone} before returning it.  Typically, this means
 * copying any mutable objects that comprise the internal "deep structure"
 * of the object being cloned and replacing the references to these
 * objects with references to the copies.  If a class contains only
 * primitive fields or references to immutable objects, then it is usually
 * the case that no fields in the object returned by {@code super.clone}
 * need to be modified.
 * <p>
 * The method {@code clone} for class {@code Object} performs a
 * specific cloning operation. First, if the class of this object does
 * not implement the interface {@code Cloneable}, then a
 * {@code CloneNotSupportedException} is thrown. Note that all arrays
 * are considered to implement the interface {@code Cloneable} and that
 * the return type of the {@code clone} method of an array type {@code T[]}
 * is {@code T[]} where T is any reference or primitive type.
 * Otherwise, this method creates a new instance of the class of this
 * object and initializes all its fields with exactly the contents of
 * the corresponding fields of this object, as if by assignment; the
 * contents of the fields are not themselves cloned. Thus, this method
 * performs a "shallow copy" of this object, not a "deep copy" operation.
 * <p>
 * The class {@code Object} does not itself implement the interface
 * {@code Cloneable}, so calling the {@code clone} method on an object
 * whose class is {@code Object} will result in throwing an
 * exception at run time.
 *
 * @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
 */

参考资料

Oracle Java 8 API文档


转载:https://blog.csdn.net/weixin_42332985/article/details/105921516
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场