Java再回顾(2)—浅拷贝与深拷贝

前言

对象的拷贝是java开发中绕不过去的话题,实际开发中运用到的也很多。
有的时候,需要一份对象的复制结果,然后对复制结果进行裁剪、修改等,可是又不能影响到原对象,那么一个简单的'b=a'肯定就再难堪大任(只针对可变对象哦,对于不可变对象这种场景压根儿不存在),于是我们的主角clone就要闪亮登场。

clone()方法

clone()是object里的protected方法:
clone()是protected 的,即包可见和可继承。猜想应该是由于不知道子类具体的clone后的返回类型,于是设置成protected的,让子类来重写该方法。

    protected Object clone() throws CloneNotSupportedException {
        if (!(this instanceof Cloneable)) {
            throw new CloneNotSupportedException("Class " + getClass().getName() +
                                                 " doesn't implement Cloneable");
        }

        return internalClone();
    }

clone()方法要求子类实现Cloneable接口

public interface Cloneable {
}

Cloneable接口是java四大接口之一,如上面代码所示,Cloneable接口只是一个标记接口,它的出现就是让使用者知道要对clone方法重写。如果没有implement这个接口,又使用了clone()方法,就会抛出异常。

转过头来,我们再看一看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()}.

里面提到:准确的copy含义也许取决于该对象的类。这样做的意图是,对于任意的一个对象,以下表达式:
x.clone() != x为真
x.clone().getClass() == x.getClass()为真
但是它们并非必须满足的要求。
通常情况下,x.clone().equals(x)为真(依旧非必须满足)。
按照惯例,clone()方法返回的对象应该由调用super.clone获得,
如果一个类和其所有除了object的超类都遵循了此惯例,那么x.clone().getClass() == x.getClass()为真。

害,绕过来绕过去的,说白了,具体情况具体对待


熊猫人.png

总的来说,要拷贝就要implement Cloneable这个接口,重写clone()方法并在其中合理地调用super.clone()。那么clone()方法返回的是当前对象的一个拷贝。而这个拷贝是浅拷贝的。

浅拷贝

前面提到了浅拷贝,辣么什么是浅拷贝呢?
这里有百度百科的一串概念:

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

结合自己的理解,我来通俗地翻译并且转化一下这段话:
在进行浅拷贝时,会为新对象分配一块空间来进行对象的拷贝。浅拷贝得到的新对象和旧对象的内存地址已经不同,但成员属性完全相同。
而这种属性分为两类:
1.基本类型
2.对象(可变对象、不可变对象(典型的就是String,在稍后的例子中我们会看到)
所以浅拷贝得到的成员属性,对于基本类型来说,就是复制一下值。第二种,则只是复制了引用哦,只是将新对象的对应属性的引用指向原引用。所以就会存在互相影响的问题(然而不可变对象是个例外,由于它的特殊性,这种互相影响就不存在)
示意图大概就是下面这样(图片来自百度百科):

百度到的图.jpg.png

下面我们来看个例子:
这是一个类,名叫人,它拥有三个成员:age(基本类型int)、name(不可变对象String)、House(可变对象)。它就覆盖了我们上面所谈到的情况。

public class Person implements Cloneable{

    private String name ;
    private int age;
    private House house;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public House getHouse() {
        return house;
    }

    public void setHouse(House house) {
        this.house = house;
    }

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

    @Override
    public String toString(){
        return "[Person: "+this.hashCode()+" name: "+name+" age: "+age+"  house:"+house+" ]";
    }

}

public class House {

    private int money;
    private String type;

    public House(int money ,String type){
        this.money=money;
        this.type=type;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    @Override
    public String toString(){
        return "[House: "+this.hashCode()+" money: "+money+" type: "+type+" ]";
    }

}

测试:

public class copyTest {
    public static void main(String[] args){
        Person person1 = new Person();
        person1.setAge(18);
        person1.setHouse(new House(20,"small house"));
        person1.setName("alex");

        Person person2 =person1;//=测试,其实这个完全不需要,但还是做个对比
        person2.setName("alice");
        person2.setAge(20);
        House house2 = person2.getHouse();
        house2.setMoney(40);

        System.out.println(person1.toString());
        System.out.println(person2.toString());

        System.out.println("华丽丽的分割线");

        Person person3 = (Person) person1.clone();//浅拷贝
        person3.setName("tom");
        person3.setAge(28);
        House house3 = person3.getHouse();
        house3.setMoney(60);
        System.out.println(person1.toString());
        System.out.println(person3.toString());

    }
}

运行结果:

[Person: 1163157884 name: alice age: 20  house:[House: 1956725890 money: 40 type: small house ] ]
[Person: 1163157884 name: alice age: 20  house:[House: 1956725890 money: 40 type: small house ] ]
华丽丽的分割线
[Person: 1163157884 name: alice age: 20  house:[House: 1956725890 money: 60 type: small house ] ]
[Person: 356573597 name: tom age: 28  house:[House: 1956725890 money: 60 type: small house ] ]

显然,浅拷贝确实新创建了一个对象,但是对于该对象内的可变对象成员的拷贝只是复制了引用。

所以person3和person1并非独立,对其中的house对象的修改会互相影响。
那么问题来了,我现在希望这两个person是完全独立的,又该怎么做呢?

这时就涉及到深拷贝。

深拷贝

说白了,就是在浅拷贝的达到的基础上再把要复制的对象里的可变对象也新建对象空间复制一份,而不是复制引用。

达到深拷贝的方式主要有两种:
1.层层浅拷贝(每一层的可变对象都给它clone)
2.序列化

下面我们先试第一种方法:

public class Person implements Cloneable{

    private String name ;
    private int age;
    private House house;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public House getHouse() {
        return house;
    }

    public void setHouse(House house) {
        this.house = house;
    }

    @Override
    public Object clone()  {
        try {
            Person person=(Person) super.clone();
            person.house=(House) house.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString(){
        return "[Person: "+this.hashCode()+" name: "+name+" age: "+age+"  house:"+house+" ]";
    }

}
public class House implements Cloneable{

    private int money;
    private String type;

    public House(int money ,String type){
        this.money=money;
        this.type=type;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

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

    @Override
    public String toString(){
        return "[House: "+this.hashCode()+" money: "+money+" type: "+type+" ]";
    }

}

测试:

public class copyTest {
    public static void main(String[] args){
        Person person1 = new Person();
        person1.setAge(18);
        person1.setHouse(new House(20,"small house"));
        person1.setName("alex");

        Person person3 = (Person) person1.clone();
        person3.setName("tom");
        person3.setAge(28);
        House house3 = person3.getHouse();
        house3.setMoney(60);
        System.out.println(person1.toString());
        System.out.println(person3.toString());

    }
}

结果:

[Person: 1163157884 name: alex age: 18  house:[House: 1956725890 money: 20 type: small house ] ]
[Person: 356573597 name: tom age: 28  house:[House: 1735600054 money: 60 type: small house ] ]
image.png

下面看一下序列化的使用方法,一个类要能序列化,就必须实现Serializable这个标记接口。
这里我们对person和House分别implements上Serializable接口后进行测试:

public class copyTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

          Person person1 = new Person();
          person1.setAge(18);
          person1.setHouse(new House(20,"small house"));
          person1.setName("alex");

          Person person3 = (Person) deepClone(person1);
          person3.setName("tom");
          person3.setAge(28);
          House house3 = person3.getHouse();
          house3.setMoney(60);
          System.out.println(person1.toString());
          System.out.println(person3.toString());

    }

    public static Object deepClone(Object object) throws IOException, ClassNotFoundException {
        //写入对象
        ByteArrayOutputStream bo=new ByteArrayOutputStream();
        ObjectOutputStream oo=new ObjectOutputStream(bo);
        oo.writeObject(object);
        //读取对象
        ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi=new ObjectInputStream(bi);
        return(oi.readObject());
    }
}
[Person: 325040804 name: alex age: 18  house:[House: 1173230247 money: 20 type: small house ] ]
[Person: 2065951873 name: tom age: 28  house:[House: 1791741888 money: 60 type: small house ] ]
image.png

后记

个人觉得浅拷贝、深拷贝的理解还是蛮重要,有时候莫名其妙的值的改变的问题很大程度就是多线程或者对拷贝的理解不透彻的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354