【Java SE】 Cloneable接口, 浅拷贝和深拷贝

【Java SE】 Cloneable接口, 浅拷贝和深拷贝,第1张

目录
  • Cloneable接口
  • 浅拷贝
  • 深拷贝

什么叫对象的拷贝? 或者克隆? 就是把这个对象完完整整的复制一份出来,两份对象是一模一样的,比双胞胎还双胞胎,那怎么样实现呢?

 public static void main(String[] args) {
     Person person1 = new Person("zhangsan", 18);
     Person person2 = new Person("zhangsan", 18)}

我们发现,这种写法,new出来的两个对象也是一模一样的,这种算是拷贝吗? 注意,这不叫拷贝。person2并不是person1拷贝而来的,是你从零生造出来的,而不是以person1为模板拷贝。

或者,也有人这样写:

 public static void main(String[] args) {
     Person person1 = new Person("zhangsan", 18);
     Person person2 = person1;
 }

这种也不是拷贝,这种写法呢person1和person2存储了同一块地址,都指向一个同一个对象,所以如果通过person2来修改对象中的值,同样person1的值也会改变。

 public static void main(String[] args) {
     Person person1 = new Person("zhangsan", 18);
     Person person2 = person1;
     //修改person2的属性
     person2.age = 20;
     System.out.println(person2.age);
     System.out.println(person1.age);
     
 }

那如何实现拷贝呢?

Cloneable接口

Object 类中存在一个 clone() 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现Cloneable接口,重写Object的clone()方法, 否则就会抛出 CloneNotSupportedException 异常。

Cloneable是一个空接口,里面没有内容,我们叫它标记接口,代表这个类是可以被拷贝的。

然后,由person1对象调用clone方法,就会在堆上完完整整地克隆一份对象,并返回这个对象的引用。

我们打开万能的Generate,选择Override Methods,重写clone()方法。


我们来看看super的clone方法,按住Ctrl,点进去。

我们发现它是一个native修饰的方法,它的底层是用C/C++实现的拷贝,我们看不到它的源码。我猜测可能是用了memcpy之类的方法对内存进行拷贝,不用细究。

native 方法是非 Java 语言实现的,是Java底层代码,供 Java 程序调用的。因为 Java 程序是运行在 JVM 虚拟机上面的,要想访问到比较底层的与 *** 作系统相关的就没办法了,只能由靠近 *** 作系统的语言来实现。clone就是一种对堆栈的 *** 作,一种底层的 *** 作。

由于clone方法的返回类型是Object类型的引用,我们在使用时,得用强转进行向下转型,因为父类可以直接接受子类引用(向上转型),而子类接受父类引用必须进行强转才行。如下所示:

这是因为clone方法声明了一个异常 CloneNotSupportedException,在没有实现Cloneable接口时会抛出,我们并没有处理它,处理办法是用try-catch语句,try关键字可以对可能出现异常的代码进行监视,如果出现异常就抛出,然后由catch关键字捕捉相应的异常进行处理。后面异常部分再细说。

class Person implements Cloneable{
    String name;
    int age;

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

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Test {
    public static void main(String[] args) {
        Person person1 = new Person("zhangsan", 18);
        try {
            Person person2 = (Person)person1.clone();
            //克隆一份person1对象,由person2接受
            System.out.println("person1: "+person1);
            System.out.println("person2: "+person2);
        } catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

我们来分析一下:

clone方法在堆中开辟空间,把person1中的数据拷贝进去,并把这块新的空间的内存地址返回,交给person2,就得到了我们的新的person2对象。

我们就完成了对象的拷贝,这样如果我们通过person2修改对象的属性,就不会影响person1了。

    public static void main(String[] args) {
        Person person1 = new Person("zhangsan", 18);
        try {
            Person person2 = (Person)person1.clone();
            System.out.println("person1: "+person1);
            System.out.println("person2: "+person2);
            person2.age = 20;
            System.out.println("==========修改后=========");
            System.out.println("person1: "+person1);
            System.out.println("person2: "+person2);
        } catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

浅拷贝

这是我们基本数据类型的拷贝,如果,我们类的属性中存在引用类型变量呢?

这里String name虽然也是引用类型,但暂不涉及String深拷贝,String是被final修饰的,name所指向的对象不能被修改,name变量本身是可以被修改的。而且’‘zhangsan’'字符串会被放在常量池,后面创建对象不会多次为对象开辟空间,在详解String时再谈吧。

如这段代码所示:
我们说拷贝时开辟一块新的空间,把原来对象的所有属性的值存进去。
那现在对person1的拷贝就成了这样:

把m引用里存的地址也拷贝了一份到person2里,那么,person2的m也引用person1的m对象。

那么理所应当,我们通过person2的m来修改money,person1的money也会改变。

    public static void main(String[] args) {
        Person person1 = new Person("zhangsan", 18);
        try {
            Person person2 = (Person)person1.clone();
            System.out.println("person1: "+person1);
            System.out.println("person2: "+person2);
            //修改年龄
            person2.age = 20;
            System.out.println("==========修改年龄后=========");
            System.out.println("person1: "+person1);
            System.out.println("person2: "+person2);
            //修改money
            person2.m.money = 999;
            System.out.println("==========修改money后========");
            System.out.println("person1: "+person1);
            System.out.println("person2: "+person2);
        } catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

为了打印出我们的money,我们修改一下Person的toString方法,加入money的打印。

看运行结果:


果然,person1的money也被修改成了999,那这里呢其实就是发生了所谓的浅拷贝,那应该如何避免这种问题呢?我们说需要进行深拷贝

是不是应该把Money对象也在内存中重新拷贝一份,而不是person1和person2共用一个Money对象。

深拷贝

要实现m对象的拷贝,是不是Money也得是能够拷贝的。 所以,Money类也得实现Cloneable接口,并且重写clone方法。

class Money implements Cloneable{
    public int money = 20;

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

我们实现好了Cloneable接口,那具体怎么实现深拷贝呢?

因为在克隆person1时,我们是通过调用Person的clone方法来实现的,所以,为了实现深拷贝,我们需要改动一下Person的clone方法,让他也能同时拷贝m对象。

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //先定义一个临时对象,让它完成浅拷贝
        Person tmp = (Person)super.clone();
        //然后让这个临时对象的m也拷贝一份
        //这样m引用的地址就是一块新的内存空间地址
        //而不是之前person1的money的地址
        tmp.m = (Money)this.m.clone();//m为Money类型,所以强转为Money
        
        //把这个临时对象返回
    	//返回的tmp是Person类型,发生向上转型转为Object进行返回
        return tmp;
        
		//return super.clone();
    }

同样是刚刚那串代码,我们来看运行结果:

   public static void main(String[] args) {
       Person person1 = new Person("zhangsan", 18);
       try {
           Person person2 = (Person)person1.clone();
           System.out.println("person1: "+person1);
           System.out.println("person2: "+person2);
           //修改年龄
           person2.age = 20;
           System.out.println("==========修改年龄后=========");
           System.out.println("person1: "+person1);
           System.out.println("person2: "+person2);
           //修改money
           person2.m.money = 999;
           System.out.println("==========修改money后========");
           System.out.println("person1: "+person1);
           System.out.println("person2: "+person2);
       } catch(CloneNotSupportedException e) {
           e.printStackTrace();
       }
   }

我们来在内存上分析代码。
第一步:

第二步:


第三步:

至此,我们便完成了深拷贝的实现。


总结一下,关于什么是深拷贝

  1. 跟拷贝的数据类型有关
  2. 跟实际代码有关

对对象中引用类型的变量的拷贝,不能单纯的拷贝引用的值(也就是地址),必须连同引用所指向的对象一同拷贝一份。


码字不易,点个赞再走吧,收藏不迷路~

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/787972.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-05
下一篇 2022-05-05

发表评论

登录后才能评论

评论列表(0条)

保存