Cloneable接口和Objec中的clone()方法

starlin 306 2018-09-20

概述

Cloneable接口是一个空接口,仅用于标记对象,Cloneable接口里面是没有clone()方法,这个clone()方法是Object类里面的方法!默认实现是一个Native方法
如果对象implement Cloneable接口的话,需要覆盖clone方法(因为Object类的clone方法是protected,需要覆盖为public)
Object类里的clone()方法仅仅用于浅拷贝(拷贝基本成员属性,对于引用类型仅返回指向改地址的引用)
结合java虚拟机来看,克隆其实就是在堆中克隆出出一块和原对象一样的对象,并将这个对象的地址赋予新的引用

Object中的clone()方法

上面的注释省略,源码如下:

protected native Object clone() throws CloneNotSupportedException;

Object 对 clone() 方法的约定有三条:

  • 对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象;
  • 对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回 true,因为克隆对象与原对象的类型是一样的;
  • 对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。

使用Cloneable接口对Java对象进行克隆

实现克隆的步骤:

  1. 让该类实现java.lang.Cloneable接口;
  2. 重写(override)Object类的clone()方法。
    普通对象的克隆是克隆属性的值,而不是引用
public class CloneDemo implements Cloneable {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
   
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneDemo cloneDemo = new CloneDemo();
        cloneDemo.setName("aa");
        System.out.println("cloneDemo---->" + cloneDemo.getName());
        CloneDemo cloneDemo1 = cloneDemo;
        cloneDemo1.setName("bb");
        System.out.println("cloneDemo---->" + cloneDemo.getName());
        System.out.println("cloneDemo1---->" + cloneDemo1.getName());

        /******************克隆使用********************/
        CloneDemo clone = new CloneDemo();
        clone.setName("clone");
        CloneDemo clone1 = (CloneDemo) clone.clone();

        System.out.println("clone 和 clone1 是否相等--->" + (clone == clone1));
        System.out.println("clone.equals(clone1)--->" + clone.equals(clone1));

        clone1.setName("clonebb");
        System.out.println("clone---->" + clone.getName());
        System.out.println("clone1---->" + clone1.getName());
    }
}

运行结果:

cloneDemo---->aa
cloneDemo---->bb
cloneDemo1---->bb
/******************克隆使用********************/
clone 和 clone1 是否相等--->false
clone.equals(clone1)--->false
clone---->clone
clone1---->clonebb

浅克隆和深克隆

浅克隆和深克隆:
浅克隆:(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的
简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象,如下图所示:
浅克隆

深克隆(Deep Clone):是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象,如下图所示:
深克隆

1. 浅克隆,Object提供一个clone()方法,它就是浅克隆即很表层的克隆,拷贝规则如下:

  • 基本类型

如果变量是基本类型,则拷贝其值,比如:int、float、long等。

  • String字符串

这个比较特殊,克隆的是地址,是个引用,但是在修改的时候,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,此处可以认为String是个基本类型。

  • 对象

如果变量是一个实例对象,则克隆地址引用(即克隆它自身以及它所包含的所有对象的引用地址),也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制,一个private修饰的变量,竟然可以被两个实例对象访问。

示例代码如下:

public class Person implements Cloneable {
    private String name;
    private Person father;

    public Person(String name) {
        this.name = name;
    }

    public Person(String name, Person father) {
        this.name = name;
        this.father = father;
    }

    /**
     * 浅拷贝
     * @return
     */
    @Override
    public Person clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        /**
         * 1.重新覆写对象实例部分的拷贝,实现深拷贝
         * 2.如果没有重写对象实例部分的拷贝,那么大儿子和小儿子的父亲会引用同一个父亲,
         * 只要任意修改一下父亲,另外一个就会被修改
         */
        p.setFather(new Person(p.getFather().getName()));
        return p;
    }

    public String getName() {
        return name;
    }

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

    public Person getFather() {
        return father;
    }

    public void setFather(Person father) {
        this.father = father;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        //定义父亲
        Person father = new Person("父亲");
        //定义大儿子
        Person son1 = new Person("大儿子",father);
        //定义小儿子,通过克隆的方式
        Person son2 = son1.clone();
        //定义小儿子的name
        son2.setName("小儿子");
        //给小儿子,找个干爹
        son2.getFather().setName("干爹");

        System.out.println(son1.getName()+" 的父亲是 "+son1.getFather().getName());
        System.out.println(son2.getName()+" 的父亲是 "+son2.getFather().getName());

    }
}

没有去掉代码中的这一段(p.setFather(new Person(p.getFather().getName())))运行结果:

大儿子 的父亲是 父亲
小儿子 的父亲是 干爹

去掉后的运行结果:我们发现,仅仅是设置了小儿子的干爹,结果大儿子的父亲也变了

大儿子 的父亲是 干爹
小儿子 的父亲是 干爹

2. 深克隆,克隆除自身对象以外的所有对象,包括自身所包含的所有对象实例

如果想要实现对对象的深克隆,在不引入第三方jar包的情况下,可以使用两种办法:

  • 先对对象进行序列化,紧接着马上反序列化出
  • 先调用super.clone()方法克隆出一个新对象来,然后在子类的clone()方法中手动给克隆出来的非基本数据类型(引用类型)赋值,比如ArrayList的clone()方法:
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

Arrays.copyOf()

如果是数组类型,我们可以直接使用 Arrays.copyOf() 来实现克隆,实现代码如下:

    Person[] p1 = {new Person("starlin")};
    Person[] p2 = Arrays.copyOf(p1,p1.length);
    p1[0].setName("smartlin");
    System.out.println("p1--->" + p1[0].getName());
    System.out.println("p2--->" + p2[0].getName());

输出结果:

    p1--->smartlin
    p2--->smartlin

从结果可以看出,我们在修改克隆对象的第一个元素之后,原型对象的第一个元素也跟着被修改了,这说明 Arrays.copyOf() 其实是一个浅克隆。

因为数组比较特殊数组本身就是引用类型,因此在使用 Arrays.copyOf() 其实只是把引用地址复制了一份给克隆对象,如果修改了它的引用对象,那么指向它的(引用地址)所有对象都会发生改变,因此看到的结果是,修改了克隆对象的第一个元素,原型对象也跟着被修改了。

深克隆实现方式汇总

深克隆的实现方式有很多种,大体可以分为以下几类:

  • 所有对象都实现克隆方法;
  • 通过构造方法实现深克隆;
  • 使用 JDK 自带的字节流实现深克隆;
  • 使用第三方工具实现深克隆,比如 Apache Commons Lang;
  • 使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等

End,感谢阅读!!!


# Java基础