Java8之深克隆与浅克隆

 参考资料:

《深克隆和浅克隆有什么区别呢?它们是怎么实现的呢?》

目录

一、深克隆与浅克隆介绍

        1、概述

        2、拷贝功能的实现

二、深克隆实现方式

        1、将所有对象都实现克隆

        2、通过构造方法实现深克隆

        3、通过字节流实现深克隆

        4、使用开源工具包

补充:

        1、为什么既要继承Cloneable接口也要重写clone方法

        2、重写clone() 方法有哪些要求


一、深克隆与浅克隆介绍

        1、概述

        java中允许将一个对象赋值出一份完全一样的对象出来,称之为克隆。克隆又分浅克隆与深克隆,他们的区别在于,对于引用类型的成员变量,浅克隆复制的是引用,而深克隆复制的是对象。

        浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的。

        简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象。

在这里插入图片描述

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

 在这里插入图片描述

        2、拷贝功能的实现

        要实现克隆则需要继承Cloneable接口,并重写Object 类中 clone()方法。

public class ProblemSolution {
    public static void main(String[] args)  {
        // 创建被赋值对象
        People p1 = new People();
        p1.setId(1);
        p1.setName("Java");
        // 克隆 p1 对象
        People p2;
        try {
            p2 = (People) p1.clone();
            System.out.println(p1.getName().equals(p2.getName()));
            System.out.println(p1.getName()==p2.getName());
        }
        catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

class People implements Cloneable {
    // 属性
    private Integer id;
    private String name;
    /**
     * 重写 clone 方法
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

        结果如下: 

true
true

        上面的代码使用了Object类自带的clone方法实现了clone,我们可以从结果中看到,两个对象中的String类型引用指向的是同一个对象,因此Object的clone方法是浅拷贝。

二、深克隆实现方式

        1、将所有对象都实现克隆

        这种方式我们需要修改 People 和 Address 类,让它们都继承Cloneable接口并重写clone方法,让所有的引用对象都实现克隆,从而实现 People 类的深克隆。

class People implements Cloneable {
    private Address address;
    // 重写clone方法,内部将每一个成员变量都进行clone
    @Override
    protected Object clone() throws CloneNotSupportedException {
        People people = (People) super.clone();
        people.setAddress((Address)this.address.clone()); // 引用类型克隆赋值
        return people;
    }
    public void setAddress(Address address) {
        this.address = address;       
    }
    public Address getAddress() {
        return address;
    }
}

class Address implements Cloneable { 
    private String city;
    // 重写clone方法
    @Override
    protected Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
    public Address(String city){
        this.city = city;
    }

    // 重写equals方法
    @Override
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof Address) {
            Address anotherAddress = (Address)anObject;
            return this.getCity().equals(anotherAddress.getCity());
        }
        return false;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
}

        将p1与其clone得到的p2进行比较。

    public static void main(String[] args)  {
        // 创建被赋值对象
        People p1 = new People();
        p1.setAddress(new Address("北京"));
        // 克隆 p1 对象
        People p2;
        try {
            p2 = (People) p1.clone();
            System.out.println(p1.getAddress().equals(p2.getAddress()));
            System.out.println(p1.getAddress()==p2.getAddress());
        }
        catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

         结果如下:

true
false

        我们看到p1与p2中的成员变量address已经是不同的对象了,深克隆实现成功。但这么做的问题在于所有的类都要实现clone方法,过于繁琐。

        

        2、通过构造方法实现深克隆

        《Effective Java》 中推荐使用构造器(Copy Constructor)来实现深克隆,如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新 new 一个对象。

    public static void main(String[] args)  {
        // 创建被赋值对象
        People p1 = new People(new Address("北京"));
        People p2 = new People(new Address(p1.getAddress().getCity()));
        System.out.println(p1.getAddress().equals(p2.getAddress()));
        System.out.println(p1.getAddress()==p2.getAddress());
    }

        结果如下:

true
false

        3、通过字节流实现深克隆

        通过 JDK 自带的字节流实现深克隆的方式,是先将要原型对象写入到内存中的字节流,然后再从这个字节流中读出刚刚存储的信息,来作为一个新的对象返回,那么这个新对象和原型对象就不存在任何地址上的共享,这样就实现了深克隆。

        简单来说,就是使用序列化存储后再读取,就可以获得一个完全一致的对象。

class Address implements Serializable{
    ...
}

class People implements Serializable{
    ...
}

class StreamClone {
    public static <T extends Serializable> T clone(People obj) {
        T cloneObj = null;
        try {
            // 写入字节流
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bo);
            oos.writeObject(obj);
            oos.close();
            // 分配内存,写入原始对象,生成新对象
            ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流
            ObjectInputStream oi = new ObjectInputStream(bi);
            // 返回生成的新对象
            cloneObj = (T) oi.readObject();
            oi.close();
        } catch (Exception e) {

            e.printStackTrace();
        }
        return cloneObj;
    }
}
    public static void main(String[] args)  {
        // 创建被赋值对象
        People p1 = new People(new Address("北京"));
        People p2 = StreamClone.clone(p1);
        System.out.println(p1.getAddress().equals(p2.getAddress()));
        System.out.println(p1.getAddress()==p2.getAddress());
    }

 结果如下:

true
false

        需要注意的是,由于是通过字节流序列化实现的深克隆,因此每个对象必须能被序列化,必须实现 Serializable 接口,标识自己可以被序列化,否则会抛出NotSerializableException异常。

        4、使用开源工具包

        这里以fastjson为例,使用 JSON 工具类会先把对象转化成字符串,再从字符串转化成新的对象,因为新对象是从字符串转化而来的,因此不会和原型对象有任何的关联,这样就实现了深克隆,其他类似的 JSON 工具类实现方式也是一样的。

    public static void main(String[] args)  {
        // 创建被赋值对象
        People p1 = new People(new Address("北京"));
        Gson gson = new Gson();
        People p2 = gson.fromJson(gson.toJson(p1), People.class);
        System.out.println(p1.getAddress().equals(p2.getAddress()));
        System.out.println(p1.getAddress()==p2.getAddress());
    }

        

补充:

        1、为什么既要继承Cloneable接口也要重写clone方法

        对于克隆为什么要这样设计,官方没有直接给出答案,我们只能凭借一些经验和源码文档来试着回答一下这个问题。

        从源码中可以看出 Cloneable 接口在JDK 1.0 就已经存在了,因此从那个时候就已经有克隆方法了,那我们怎么来标识一个类级别对象拥有克隆方法呢?克隆虽然重要,但我们不能给每个类都默认加上克隆,这显然是不合适的,那我们能使用的手段就只有这几个了:

  • 在类上新增标识,此标识用于声明某个类拥有克隆的功能,像 final 关键字一样;
  • 使用 Java 中的注解;
  • 实现某个接口;
  • 继承某个类。

        先说第一个,为了一个重要但不常用的克隆功能, 单独新增一个类标识,这显然不合适;再说第二个,因为克隆功能出现的比较早,那时候还没有注解功能,因此也不能使用;第三点基本满足我们的需求,第四点和第一点比较类似,为了一个克隆功能需要牺牲一个基类,并且 Java 只能单继承,因此这个方案也不合适。采用排除法,无疑使用实现接口的方式是那时最合理的方案了,而且在 Java 语言中一个类可以实现多个接口。

        那为什么要在 Object 中添加一个 clone() 方法呢?

        因为 clone() 方法对时间和效率的要求很高,因此最好能有 JVM 的直接支持,既然要 JVM 直接支持,就要找一个 API 来把这个方法暴露出来才行,最直接的做法就是把它放入到一个所有类的基类 Object 中,这样所有类就可以很方便地调用到了。

        2、重写clone() 方法有哪些要求

        首先看下源码,clone()是使用 native 修饰的本地方法,因此执行的性能会很高,并且它返回的类型为 Object,因此在调用克隆之后要把对象强转为目标类型才行。虽然我们看不到clone()方法的具体实现,但我们可以从注释中看出一些信息。

/**
 * 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>
 * ......
 */
protected native Object clone() throws CloneNotSupportedException;

        从注释中可以看出:

  (1)对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象;

  (2)对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回true,因为克隆对象与原对象的类型是一样的;

  (3)对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值