java的clone()用法,深复制和浅复制、原型模式及复制工具类

一、关于clone的使用场景

对象的克隆——原型模式

在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象。原型模式的定义如下: 原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式是一种对象创建型模式。原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。

需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。

二、Clone()的用法

学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。如下代码所示:

package com.test.clone;

import lombok.Builder;
import lombok.Data;
import lombok.ToString;

@Data
public class CloneObject implements Cloneable {


    private String content;

    @Override
    public CloneObject clone()   {
        try {
            return (CloneObject) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

   

}

此处使用了lombok减少的代码量,关于lombok的用法请自行百度(异常简单),测试方法如下:


public class CloneTest {

    public static void main(String[] args) {

        CloneObject obj1 = new CloneObject();
        obj1.setContent("内容1");
        CloneObject obj2 =  null;
        obj2 = obj1.clone();
        System.out.println("clone对象两者是否是同一对象:"+(obj1==obj2));
        System.out.println(obj1.getContent());
        System.out.println(obj2.getContent());
        System.out.println("修改后不影响clone对象");
        obj1.setContent("内容2");
        System.out.println(obj1.getContent());
        System.out.println(obj2.getContent());



    }
}

运行结果:

clone对象两者是否是同一对象:false
内容1
内容1
修改后不影响clone对象
内容2
内容1

 

一般而言,Java语言中的clone()方法满足:
(1) 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
(2) 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
(3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
(1) 在派生类中覆盖基类的clone()方法,并声明为public;
(2) 在派生类的clone()方法中,调用super.clone();
(3)派生类需实现Cloneable接口。此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。

三、Clone()和深浅克隆

介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

1.浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制,如图所示:


浅克隆示意图
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。为了方便理解。我们在前例中间的CloneObject类中增加一个引用类型的成员变量Attachment(附件),其对应的Class代码如下:

package com.test.clone;

import lombok.Data;

@Data
public class Attachment implements Cloneable{
    private String name;

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

    public void dowmload(){
        System.out.println("下载附件:"+name);
    }

  
}

CloneObject类中新增Attachment对象:


@Data
public class CloneObject implements Cloneable {


    private String content;

    //新增附件对象
    private Attachment attachment;

    @Override
    public CloneObject clone()   {
        try {
            return (CloneObject) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

   

}

测试方法如下:


public class ShallowCloneTest {

    public static void main(String[] args) {

        CloneObject obj1 = new CloneObject();
        obj1.setContent("内容1");
        obj1.setAttachment(new Attachment("附件1"));
        CloneObject obj2 = obj1.clone();
        System.out.println("测试浅复制,指向同一个对象:"+( obj2.getAttachment()==obj1.getAttachment()));
        obj1.getAttachment().setName("附件2");
        System.out.println("测试浅复制,被复制对象的引用的对象改变,复制对象也同样改变,原型:"+obj1.getAttachment().getName()+"   克隆:"+obj2.getAttachment().getName());
        obj1.setAttachment(new Attachment("附件3"));
        System.out.println("测试浅复制,有意思的是被复制对象的引用改变,复制对象还会指向原来的引用,所以不会改变,原型:"+obj1.getAttachment().getName()+"  克隆: "+obj2.getAttachment().getName());


    }
}

运行后:

测试浅复制,指向同一个对象:true
测试浅复制,被复制对象的引用的对象改变,复制对象也同样改变,原型:附件2   克隆:附件2
测试浅复制,有意思的是被复制对象的引用改变,复制对象还会指向原来的引用,所以不会改变,原型:附件3  克隆: 附件2

 

由于使用的是浅克隆技术,通过“==”比较原型对象和克隆对象的内存地址时输出false;但是比较附件对象的内存地址时输出true,说明它们在内存中是同一个对象。当附件对象本身的属性发生改变是,原型和克隆的附件都发生改变。当原型指向一个新的附件后,两者的附件就无关联,这也是一种基于Cloneable接口的深复制原理。

2.深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制,如图所示:

深克隆示意图

在浅度克隆的基础上对于要克隆对象中的非基本数据类型的属性对应的类也实现克隆,这样对于非基本数据类型的属性复制的不是一份引用。CloneObject 在原来的基础上增加了一个深复制的方法,代码段如下:


@Data
public class CloneObject implements Cloneable {


    private String content;

    private Attachment attachment;

    @Override
    public CloneObject clone()   {
        try {
            return (CloneObject) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
    //深复制
    public CloneObject deepClone()   {
        CloneObject cloneObject = null;
        try {
            cloneObject =  (CloneObject) super.clone();
            //复制附件对象
            cloneObject.setAttachment(attachment.clone());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return cloneObject;
    }

}

需要注意的是,附件对象也需要实现clone()方法,代码段如下:


@Data
public class Attachment implements Cloneable{
    private String name;

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

    public void dowmload(){
        System.out.println("下载附件:"+name);
    }

    @Override
    public Attachment clone()   {
        try {
            return (Attachment) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

 

测试方法:


public class DeepCloneTest {

    public static void main(String[] args) {

        CloneObject obj1 = new CloneObject();
        obj1.setContent("内容1");
        obj1.setAttachment(new Attachment("附件1"));
        CloneObject obj2 = obj1.deepClone();
        System.out.println("测试接口深复制,是否指向同一个对象:"+( obj2.getAttachment()==obj1.getAttachment()));
        obj1.getAttachment().setName("附件2");
        System.out.println("测试接口深复制,被复制对象的引用的对象改变,复制对象不发生改变,原型:"+obj1.getAttachment().getName()+"   克隆:"+obj2.getAttachment().getName());

    }
}

运行后显示:

测试接口深复制,是否指向同一个对象:false
测试接口深复制,被复制对象的引用的对象改变,复制对象不发生改变,原型:附件2   克隆:附件1

 

证明已经进行深克隆,两者的成员变量分别指向不同的附件对象。

四、更简便的深克隆方法及其工具类

如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。下面我们使用深克隆技术来实现克隆对象和附件对象的复制,注意:由于这两个对象都写入流中,因此两个类均需要实现Serializable接口。

克隆对象:

@Data
public class CloneObject implements Serializable {

    private String content;

    private Attachment attachment;


}

附件对象:

@Data
public class Attachment implements Serializable {
    private String name;

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

    public void dowmload(){
        System.out.println("下载附件:"+name);
    }


}

基于clone()的实现方法,由于必须要Object的clone()且该方法是protect的,只供本身和其子类使用,所以无法构建工具类,但是采用序列化的方法不受此限制,只要确保对象及其引用类型的对象实现了Serializable 接口即可。工具类代码段如下:


public class BeanUtil {

    public static Object deepClone(Object obj){
        //将对象写入流中
        ByteArrayOutputStream bao=new ByteArrayOutputStream();
        ObjectOutputStream oos= null;
        try {
            oos = new ObjectOutputStream(bao);
            oos.writeObject(obj);


        } catch (IOException e) {
            e.printStackTrace();
        }
        //将对象从流中取出
        ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois= null;
        try {
            ois = new ObjectInputStream(bis);
            return ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;

    }


}

测试方法如下:

public class DeepCloneTest {

    public static void main(String[] args) {
        CloneObject obj1 = new CloneObject();
        obj1.setContent("内容1");
        obj1.setAttachment(new Attachment("附件1"));
        CloneObject obj2 = (CloneObject) CloneUtil.deepClone(obj1);
        System.out.println("测试序列化深复制,是否指向同一个对象:"+( obj2.getAttachment()==obj1.getAttachment()));
        obj1.getAttachment().setName("附件2");
        System.out.println("测试序列化深复制,被复制对象的引用的对象改变,复制对象不发生改变,原型:"+obj1.getAttachment().getName()+"   克隆:"+obj2.getAttachment().getName());

    }
}

运行结果:

测试序列化深复制,是否指向同一个对象:false
测试序列化深复制,被复制对象的引用的对象改变,复制对象不发生改变,原型:附件2   克隆:附件1

 

从输出结果可以看出,由于使用了序列化技术,附件对象也得以复制,因此用“==”比较原型对象的附件和克隆对象的附件时输出结果均为false。基于序列化的深克隆技术实现了原型对象和克隆对象的完全独立,对任意克隆对象的修改都不会给其他对象产生影响,是一种更为理想的克隆实现方式。

Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

 

 

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值