欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java基础深度总结:Object类-clone方法

程序员文章站 2024-03-14 09:17:54
...
凡心所向,素履所往,生如逆旅,一苇以航。

1.clone概述

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

protected native Object clone() throws CloneNotSupportedException;

2.Cloneable接口

如果想要使用clone方法,只覆盖Object的clone方法会抛出CloneNotSupportedException异常。

要想正确使用,该对象的类还要实现一个Cloneable标识接口。

public class CloneExample implements Cloneable {

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

Tips:

  • clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。
  • Cloneable接口的特殊之处在于: Cloneable接口中没有任何方法,它存在的意义就是允许其实现类使用clone方法。
public interface Cloneable {
}

3.clone与new的区别

  • new操作符本意实在堆中开辟内存空间。程序执行到new时,会根据new后面的类型来计算分配的内存空间大小,分配完空间以后,调用构造函数填充对象的各个域,然后返回该对象的地址。
  • 在调用clone方法时,程序会分配一块与原对象相同大小的内存空间,然后用原对象的各个域填充到新对象相应的各个域中,然后返回新对象的地址。
  • 由于native方法通常比非native方法更加高效,所以clone方法效率更高

4.浅拷贝与深拷贝(重点)

当被拷贝的对象中包含引用类型的域时,那么对该域有两种拷贝方式:

  • 将原对象中该域的引用值直接拷贝给新对象。
  • 根据原对象中该域所引用的对象,创建一个新的、与之相同的对象,然后将该对象的地址赋值给新对象中该域的引用。

前者就是浅拷贝、后者为深拷贝,例如:

Java基础深度总结:Object类-clone方法
可以看出来,在浅拷贝中,虽然两个Person对象是不一样的,但是他们的name却引用的同一个对象,这是由于浅拷贝是将引用类型的引用值直接拷贝到新对象中,那么这两个引用必然会指向同一个对象了;而深拷贝是对引用类型进行了递归clone,创建了另一个对象。

Tip:

  • 浅拷贝并没有彻底的将两个对象独立出来
  • 彻底的深拷贝需要将引用链上的所有对象都进行递归clone,比如 body对象引用了head,head对象中又引用了face:
    Java基础深度总结:Object类-clone方法

5.Object.clone

那么Object中的clone方法是深拷贝还是浅拷贝?答案是:浅拷贝。

public class CloneTest {

    public static void main(String[] args) throws CloneNotSupportedException {

        Cat cat = new Cat("Tom", 3);
        Cat tom = (Cat) cat.clone();
        System.out.println(cat == tom); //false
        System.out.println(tom.getName() == cat.getName()); // true

        tom.setName("Tom1");
        System.out.println(cat.getName() +"---" + tom.getName()); // Tom --- Tom1
    }
}

class Cat implements Cloneable {

    private String name;
    private int age;

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

}

解析:
Cat类中的name是引用类型,如果说原对象和拷贝出来的新对象的name的引用值是相同的,那么就可以说明Object.clone是浅拷贝的,反之代表Object.clone是深拷贝的。从输出结果 System.out.println(tom.getName() == cat.getName()); //true 可以看出来Object.clone确实是浅拷贝。注意: == 比较的是对象的地址。

6.浅拷贝存在的问题

如果对象中包含的域引用了可变的对象,使用Object.clone可能会导致灾难性的后果。比如一个用数组实现的Stack,并且你希望它是可以被clone的。

import java.util.Arrays;

public class ObjectCloneTest{

    public static void main(String[] args) throws CloneNotSupportedException {

        Stack stack = new Stack();
        Stack stack1 = (Stack) stack.clone();
        stack1.push(1);
        System.out.println(stack.toString());//Stack{data=[1, null, null, null, null, null, null, null, null, null], size=0}
        System.out.println(stack1.toString());//Stack{data=[1, null, null, null, null, null, null, null, null, null], size=1}
    }
}

class Stack implements Cloneable{

    private Object[] data;
    private int size;

    public Stack(){
        data = new Object[10];
        size = 0;
    }

    public void push(Object elem){
        if(size == 10) return;
        data[size] = elem;
        size++;
    }

    public Object pop(){
        if(size == 0) return null;
        size --;
        return data[size];
    }

    public int getSize() {
        return size;
    }

    public Object[] getData() {
        return data;
    }

    @Override
    public String toString() {
        return "Stack{" +
                "data=" + Arrays.toString(data) +
                ", size=" + size +
                '}';
    }

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

当你使用clone出来的对象时,你就会发现很多严重的错误,比如在clone出的对象中添加一个元素,原对象中的size就和数组的实际长度不一致了。这是由于这两个对象的data都引用了同一个数组对象。你应该在data数组中递归地调用clone:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //return super.clone();
        Stack cpyStack = (Stack) super.clone();
        cpyStack.data = cpyStack.data.clone();
        return cpyStack;
    }

Tip:
只有可变对象的引用才会出现上述问题,不可变类(IMMUTABLE CLASS)的引用,如String、基本类型的包装类、BigInteger和BigDecimal等,每次对不可变对象的修改都将产生一个新的不可变对象,因此无论修改clone出的对象的可变对象,还是修改原对象中的可变对象,都会导致可变对象的引用值发生变化,而不是可变对象本身发生变化。

例如:Cat示例中修改了 clone出的tom对象中name的值,这会导致tom对象中的name指向新的字符串对象:“Tom1”,而不会影响cat原对象中name所指向的对象。
Java基础深度总结:Object类-clone方法

7.覆盖clone的规则

由于浅拷贝并不能保证clone出的对象和原对象完全独立,所以在很多时候会导致这样那样的问题,子类覆盖clone一般都是实现深拷贝。

  • 首先调用父类super.clone方法(父类必须实现clone方法),这个方法最终会调用Object中的clone方法完成浅拷贝。
  • 对类中的引用类型进行单独拷贝。
  • 检查clone中是否有不完全拷贝,进行额外的复制。

8.clone的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

  • 拷贝构造器: public Yum(Yum yum);
  • 拷贝工厂: public static Yum newInstance(Yum yum);
  • 使用序列化