java~深拷贝和浅拷贝(clone)

Java中的深拷贝和浅拷贝

写在前面:
   没学习JAVA之前,想必大家都听过“克隆羊”这个词。克隆相当于复制的意思,但和复制的概念并不相同。克隆在百度上面的翻译如下:
在这里插入图片描述  Java中的克隆是克隆相对与类的实例来说的,克隆的是对象的一个副本。
  采用的设计模式:原型模式(Prototype)


1. 什么时候使用克隆?

  • 打个比方:你下班回家的路上,看的一家蛋糕店,恰巧你想吃蛋糕了,然后你进门看到模型的蛋糕。此时你并不知道每个蛋糕的名字,你指着你想要的蛋糕和销售员说要这个,然后,制作人员就克隆出一块一模一样的给你了。
  • eg:方法需要返回引用,但是又不希望用户对该引用指向的对象进行修改。这时候直接返回clone()的结果
class Test {
    private static int[] value;
    static {
        value = new int[]{1, 2, 3, 4, 5};
    }
    public static final int[] getValue() {
        return value.clone();
    }
}

2. 克隆的概念?

  深浅克隆都会在堆中新分配一块区域,区别在于对象属性引用的对象是否需要进行克隆(详情看下面源码的注释部分)

  • 浅克隆:
      创建一个新对象,新对象的属性 和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
  • 深克隆:
      创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
  • 源码分析:
     1.Cloneable接口
    源码翻译是不是看不懂,不用怕源码翻译神器
/**
此处源码注释省略,直接上翻译。
 1. 一个实现了Cloneable类意味着可以通过java.lang.Object的clone()合法的对该类的实例的属性逐一复制。
 2. 对一个没有实现Cloneable接口的类的实例调用clone()会抛出
 3. 按照惯例,实现了此接口的类应该重写clone(),重写时将该方法由受保护变为公开。
 4. 需要注意的是,此接口并不包含clone()。因此,仅依靠实现此接口是不可能实现对象克隆的。即使clone()被成功调用,也不能保证克隆可以成功。
 **/
	public interface Cloneable {
	}

  2. Object类中clone方法

/**
此处源码注释省略,直接上翻译。
 1.创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。这样做的目的是,对于任何对象 x,表达式:x.clone() != x为 true,表达式:x.clone().getClass() == x.getClass()也为 true,但这些并非必须要满足的要求。一般情况下:x.clone().equals(x)为 true,但这并非必须要满足的要求。按照惯例,返回的对象应该通过调用 super.clone 获得。如果一个类及其所有的超类(Object 除外)都遵守此约定,则 x.clone().getClass() == x.getClass()。
 2.按照惯例,此方法返回的对象应该独立于该对象(正被复制的对象)。要获得此独立性,在 super.clone 返回对象之前,有必要对该对象的一个或多个字段进行修改。这通常意味着要复制包含正在被复制对象的内部“深层结构”的所有可变对象,并使用对副本的引用替换对这些对象的引用。如果一个类只包含基本字段或对不变对象的引用,那么通常不需要修改 super.clone 返回的对象中的字段。Object 类的 clone 方法执行特定的复制操作。
 4. 如果此对象的类不能实现接口 Cloneable,则会抛出CloneNotSupportedException。注意,所有的数组都被视为实现接口 Cloneable。否则,此方法会创建此对象的类的一个新实例,并像通过分配那样,严格使用此对象相应字段的内容初始化该对象的所有字段;这些字段的内容没有被自我复制。所以,此方法执行的是该对象的“浅表复制”,而不“深层复制”操作。
 Object 类本身不实现接口 Cloneable,所以在类为 Object 的对象上调用 clone 方法将会导致在运行时抛出异常。
  **/
    protected native Object clone() throws CloneNotSupportedException;

  克隆调用的是Object类的clone()方法,clone()是一个本地方法(native),默认的修饰符是protected;

3. 浅克隆(浅拷贝)

  • 实现Cloneable接口,User类中重写Object类中的clone方法的【浅拷贝】
//User实体类
package cn.com;
import java.lang.Cloneable;
public class User implements Cloneable{

    private int uid;        //基本类型
    private String uname;   //引用类型
    private Integer uage;   //引用类型
    private Integer money;  //引用类型
    private Quote quote;    //引用类型
    
  //此处省略Getter和Setter方法
  
    @Override
    public String toString() {
        return "User{" +
                "uid=" + uid +
                ", uname='" + uname + '\'' +
                ", uage=" + uage +
                ", money=" + money +
                ", quote=" + quote +
                '}';
    }
    @Override
    public Object clone() throws CloneNotSupportedException{
      User user= (User) super.clone();
      return user;
    }
}

//自定义实体类用作演示引用类型
package cn.com;
public class Quote {
    private int qid;    //基本类型
    private String qname;//引用类型
    private Integer qage;//引用类型
    
      //此处省略Getter和Setter方法
      
    @Override
    public String toString() {
        return "Quote{" +
                "qid=" + qid +
                ", qname='" + qname + '\'' +
                ", qage=" + qage +
                '}';
    }
}
//克隆测试类
package cn.com;
public class CloneTest {
    public static void main(String[] args) {
        User user=new User();
        user.setUid(1001);
        user.setUname("user");
        user.setUage(18);
        user.setMoney(128);

        Quote quote=new Quote();
        quote.setQid(2002);
        quote.setQage(20);
        quote.setQname("quote");
        user.setQuote(quote);

        try {
            System.out.println("***************实现Cloneable接口,User类中重写Object类中的clone方法的【浅拷贝】***************");
            User userClone= (User) user.clone();
            System.out.println("原始对象"+user.toString());
            System.out.println("克隆对象"+userClone.toString());
            System.out.println("判断原始对象和克隆对象   "+(user==userClone));
            System.out.println("判断两个对象中基本类型:  "+(user.getUid()==userClone.getUid()));
            System.out.println("判断两个对象中String类型: "+(user.getUname()==userClone.getUname()));
            System.out.println("判断两个对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
            System.out.println("判断两个对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
            System.out.println("判断两个对象中引用类型:  *************"+(user.getQuote()==userClone.getQuote()));
            System.out.println();
                // 修改浅拷贝对象的属性(基本类型和包装类型不变)
            userClone.getQuote().setQage(40);
            userClone.getQuote().setQname("QuoteCloneName");
            userClone.getQuote().setQid(4004);
            System.out.println("***************克隆后修改引用类型Quote的值打印比较***************");
            System.out.println("原始对象"+user.toString());
            System.out.println("克隆对象"+userClone.toString());
            System.out.println("判断原始对象和修改后的克隆对象中基本类型:  "+(user.getUid()==userClone.getUid()));
            System.out.println("判断原始对象和修改后的克隆对象中String类型: "+(user.getUname()==userClone.getUname()));
            System.out.println("判断原始对象和修改后的克隆对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
            System.out.println("判断原始对象和修改后的克隆对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
            System.out.println("判断原始对象和修改后的克隆对象中引用类型:  *******"+(user.getQuote()==userClone.getQuote()));
            System.out.println("***********************克隆后修改基本类型和包装类型的值打印比较**************************");
            userClone.setUid(1002);
            userClone.setUname("userName");
            userClone.setUage(18);
            userClone.setMoney(128);  //Money如果是127的话,输出为true
            System.out.println("原始对象"+user.toString());
            System.out.println("克隆对象"+userClone.toString());
            System.out.println("判断原始对象和修改后的克隆对象中基本类型:  "+(user.getUid()==userClone.getUid()));
            System.out.println("判断原始对象和修改后的克隆对象中String类型: "+(user.getUname()==userClone.getUname()));
            System.out.println("判断原始对象和修改后的克隆对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
            System.out.println("判断原始对象和修改后的克隆对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
            System.out.println("判断原始对象和修改后的克隆对象中引用类型:  *******"+(user.getQuote()==userClone.getQuote()));
            System.out.println("*********************************验证String和Integer类型*************************************");

            userClone.setUname("user");     //对于String类型,每次赋不同的值都会产生新的字符串对象放在常量池中,相同的值就会指向同一个引用地址
            userClone.setUage(18);
            userClone.setMoney(128);        //Money如果是127的话,输出为true
            System.out.println("原始对象"+user.toString());
            System.out.println("克隆对象"+userClone.toString());
            System.out.println("判断原始对象和修改后的克隆对象中String类型: "+(user.getUname()==userClone.getUname()));
            System.out.println("判断原始对象和修改后的克隆对象中Integer类型1: "+(user.getUage()==userClone.getUage()));
            System.out.println("判断原始对象和修改后的克隆对象中Integer类型2: "+(user.getMoney()==userClone.getMoney()));
            System.out.println("判断原始对象和修改后的克隆对象中引用类型:  *******"+(user.getQuote()==userClone.getQuote()));
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

    }
}
//打印结果
***************实现Cloneable接口,User类中重写Object类中的clone方法的【浅拷贝】***************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=2002, qname='quote', qage=20}}
克隆对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=2002, qname='quote', qage=20}}
判断原始对象和克隆对象   false
判断两个对象中基本类型:  true
判断两个对象中String类型: true
判断两个对象中Integer类型1true
判断两个对象中Integer类型2true
判断两个对象中引用类型:  *************true

***************克隆后修改引用类型Quote的值打印比较***************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
克隆对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
判断原始对象和修改后的克隆对象中基本类型:  true
判断原始对象和修改后的克隆对象中String类型: true
判断原始对象和修改后的克隆对象中Integer类型1true
判断原始对象和修改后的克隆对象中Integer类型2true
判断原始对象和修改后的克隆对象中引用类型:  *******true
***********************克隆后修改基本类型和包装类型的值打印比较**************************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
克隆对象User{uid=1002, uname='userName', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
判断原始对象和修改后的克隆对象中基本类型:  false
判断原始对象和修改后的克隆对象中String类型: false
判断原始对象和修改后的克隆对象中Integer类型1true
判断原始对象和修改后的克隆对象中Integer类型2false
判断原始对象和修改后的克隆对象中引用类型:  *******true
*********************************验证String和Integer类型*************************************
原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
克隆对象User{uid=1002, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}
判断原始对象和修改后的克隆对象中String类型: true
判断原始对象和修改后的克隆对象中Integer类型1true
判断原始对象和修改后的克隆对象中Integer类型2false
判断原始对象和修改后的克隆对象中引用类型:  *******true

  总结:由打印输出可以看到,修改自己写的引用类型的参数,会影响克隆之前的值,因此这是浅拷贝的形式
  我们可以观察到String类型和Integer类型克隆后修改的参数后,原始数据不会改变,但是,修改的结果为相同的时候,为什么原始数据不会改变,但是比较时地址相同呢?
  首先,String类型和包装类克隆后的参数不会改变是因为:String类型和包装类型自身的一些特性,final关键字和没有为value提供Setter方法,导致参数传递时类似值传递的方式。
  为什么比较地址时,结果相同呢?这是因为String类型和包装类型的缓存池问题,具体缓存池可查看,我的另一篇博客缓存池。

4. 深克隆(深拷贝)

  • 每个引用类型属性内部都重写clone()方法

  Quote实现Cloneable接口,并重写clone方法

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

  User修改clone方法

    @Override
    public Object clone() throws CloneNotSupportedException{
        User user=(User)super.clone();
        Quote quo= (Quote) quote.clone();
        user.setQuote(quo);
      return user;
    }

  至此便可实现深拷贝,但是这样做的弊端是如果类中有多个引用类型的属性就需重写多个clone方法,这样做很麻烦,这是我们就可以使用序列化的方式实现对象的克隆。

  • 实现Serializable接口,通过对象序列化和反序列化实现克隆
    拓展

   标识接口是没有任何方法和属性的接口,它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。使用标记接口的唯一目的是使得可以用 instanceof 进行类型查询,例如:
  if(quote instanceof Cloneable){
            Quote quo= (Quote) quote.clone();
            user.setQuote(quo);
        }

   1. java.io.Serializable:未实现此接口的类将无法使其任何状态序列化或反序列化。为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。
  2.java.lang.Cloneable:表明 Object.clone() 方法可以合法地对该类实例进行按字段复制.实现此接口的类应该使用公共方法重写 Object.clone(它是受保护的)。如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。
  3.java.util.RandomAccess:用来表明其支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
  4.java.rmi.Remote:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。


    //序列化与反序列静态方法
       public static  <T extends Serializable> T clone(T user){
        try {
            //序列化
            ByteArrayOutputStream bout=new ByteArrayOutputStream();
            ObjectOutputStream ooStream=new ObjectOutputStream(bout);
            ooStream.writeObject(user);
            //反序列化
            ByteArrayInputStream bint=new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream ooStream2=new ObjectInputStream(bint);
            return (User)ooStream2.readObject();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
	//User和Quote实体类实现Seriablizable接口,其它不变

打印信息如下

原始对象User{uid=1001, uname='user', uage=18, money=128, quote=Quote{qid=2002, qname='quote', qage=20}}
克隆对象User{uid=1002, uname='user', uage=18, money=128, quote=Quote{qid=4004, qname='QuoteCloneName', qage=40}}

  可以看到修改quote引用类型后,原始对象的值依然保持不变。说明这两个对象在内存中时相互独立的,对象间值的修改互不影响。

5. 总结

  实现Serializable接口,通过对象序列化和反序列化实现深拷贝的形式是通过泛型进行限定的,在程序编译期就可以判断出要克隆对象是否支持序列化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值