Java中的深復(fù)制和淺復(fù)制

復(fù)制和粘貼

約在7萬(wàn)多年前躲庄,我們的智人祖先經(jīng)歷了一場(chǎng)所謂的"認(rèn)知革命"。這場(chǎng)革命就像是一把鑰匙飒泻,打開(kāi)了潘多拉的魔盒鞭光,人類(lèi)的對(duì)于虛構(gòu)世界的腦洞從此一開(kāi)不可收拾。同人類(lèi)其他眾多的幻想一樣泞遗,對(duì)人事物的“復(fù)制“的這一虛構(gòu)臆想惰许,推進(jìn)了文明的演進(jìn),直接或間接地催促了藝術(shù)這種文化形態(tài)的繁榮史辙。

而現(xiàn)今汹买,隨著各種終端的普及佩伤,”復(fù)制“這個(gè)詞也隨著互聯(lián)網(wǎng)一起傳播出去。無(wú)論是你每天在電腦里使用ctrl+cctrl+v快捷鍵晦毙,還是各種網(wǎng)站對(duì)數(shù)字資源的二次分發(fā)生巡,都屬于“復(fù)制”這一范疇。而這一切的基礎(chǔ)见妒,無(wú)外乎計(jì)算機(jī)對(duì)信息載體的編碼和解碼障斋,然后就被電信號(hào)傳播。

你會(huì)不會(huì)和我一樣徐鹤,忍不住地要去幻想,若未來(lái)人類(lèi)復(fù)雜的思想也能被編碼成一串串字節(jié)碼邀层,那時(shí)候的世界又將會(huì)是怎樣呢返敬?
然而正文內(nèi)容和這個(gè)引子并沒(méi)太大的關(guān)系

JVM在等號(hào)賦值的時(shí)候都干了些什么?

定義一個(gè)Parent類(lèi)和Child類(lèi)


     private class Parent {

        public Parent() {

        }

        protected void test() {
           // do sth ...
        }

        static {
            // do sth ...
        }

    }

    private class Child extends Parent {

        public Child() {
            // do sth ...
        }

        @Override
        protected void test() {
            super.test();
            // do sth ...
        }

        static {
            // do sth ,,,
        }

    }

靜被變量和常量先行

在類(lèi)在容器初始化時(shí)寥院,JVM會(huì)按照順序自上而下運(yùn)行類(lèi)中的靜態(tài)語(yǔ)句/塊或常量劲赠,如果有父類(lèi),則首先按照順序運(yùn)行靜態(tài)語(yǔ)句/塊或常量秸谢。初始化類(lèi)的行為有且僅有一次凛澎。

這一過(guò)程中,JVM會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)Class對(duì)象的實(shí)例估蹄,指向我們初始化后的這個(gè)類(lèi)塑煎。這個(gè)也被稱(chēng)作為方法區(qū)。
此時(shí)并沒(méi)有實(shí)例化該對(duì)象臭蚁。

在堆內(nèi)存創(chuàng)建實(shí)例


    public static void main(String args[]) {
        Child child = new Child();
    }

main(String args[])標(biāo)志著這是一個(gè)主方法入口

main方法中最铁,類(lèi)又會(huì)按照這個(gè)順序執(zhí)行全局變量的賦值,然后執(zhí)行父類(lèi)的無(wú)參構(gòu)造函數(shù)和子類(lèi)的構(gòu)造函數(shù)垮兑。

在棧幀中冷尉,JVM會(huì)提前分配內(nèi)存地址用以?xún)?chǔ)存方法參數(shù)與局部變量。在這個(gè)例子中系枪,儲(chǔ)存的是args(如果有的話(huà))雀哨,和child在堆上的引用。
child對(duì)象會(huì)在堆內(nèi)存中被實(shí)例化私爷,其中包含它(及它父類(lèi))的成員變量(名稱(chēng)和具體值或指針)和方法(名稱(chēng)和具體實(shí)現(xiàn))的索引雾棺。
靜態(tài)成員變量會(huì)保存一個(gè)引用地址

入棧和出棧


   public static void main(String args[]) {
        Child child = new Child();
        child.test();
    }

執(zhí)行test()方法時(shí),會(huì)執(zhí)行父類(lèi)的同名方法衬浑,再執(zhí)行子類(lèi)的邏輯垢村。
因?yàn)榇朔椒▓?zhí)行了super.test(),而不是如隱形調(diào)用

而在內(nèi)存操作里嚎卫,此時(shí)會(huì)有一個(gè)新的棧幀被壓入棧中嘉栓,同樣的宏榕,該棧幀保存了方法中傳入的參數(shù)和局部變量。

由于該方法被其他方法調(diào)用(這里是main()方法)侵佃,棧幀中還有一個(gè)區(qū)域會(huì)保存main()方法的返回地址麻昼,這個(gè)區(qū)域被稱(chēng)作VM元數(shù)據(jù)區(qū)。在test()方法結(jié)束時(shí)馋辈,它將被推出棧抚芦。并且根據(jù)元數(shù)據(jù)區(qū)的返回地址,正確地跳回到main()方法中迈螟。
在拋出異常時(shí)叉抡,可以看到一層層的Stack Trace

而如果該方法有一個(gè)返回值,這個(gè)又該如何傳遞給調(diào)用方呢答毫?


    private class Parent {

        ...

        protected String test() {
           return "EvinK " + "is Awesome!";
        }

        ...
    }

    private class Child extends Parent {

        ...

        @Override
        protected String test() {
            String str = super.test();
            return str;
        }

        ...

    }

操作數(shù)棧在這個(gè)步驟中褥民,發(fā)揮了重要的作用。它屬于棧幀的一個(gè)組成部分洗搂,JVM臨時(shí)用它來(lái)存放需要計(jì)算的變量消返,然后將計(jì)算的結(jié)果推出到棧幀的局部變量區(qū)。

區(qū)域/棧幀 return語(yǔ)句 super.test() str = super.test() return語(yǔ)句
局部變量區(qū) str = "EvinK is Awesome!"
操作數(shù)棧 EvinK EvinK is Awesome! 指向局部變量str
- is Awesome!

使用等號(hào)復(fù)制時(shí)耘拇,發(fā)生了什么


    private class Child extends Parent {

        public String name;

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

        ...
    }

    public static void main(String args[]) {
        Child child = new Child("小明");
        Child child2 = child;
    }

前面已經(jīng)說(shuō)了撵颊,使用new關(guān)鍵字時(shí),會(huì)在堆內(nèi)存中存放該類(lèi)的實(shí)例惫叛。而棧中倡勇,會(huì)儲(chǔ)存這個(gè)在堆內(nèi)存中這個(gè)實(shí)例的引用。

而child2這個(gè)對(duì)象之間由child賦值嘉涌,也會(huì)在棧幀中的變量區(qū)译隘,創(chuàng)建一個(gè)指向這個(gè)實(shí)例在堆內(nèi)存地址的引用。


    child2.name = "EvinK"; // -> child.name = "EvinK"

    // == 比較的是對(duì)象間的引用
    System.out.print(child2 == child); // always true

正是因?yàn)檫@兩個(gè)變量指向了同一個(gè)內(nèi)存地址洛心,所以只要修改這兩者中的任何一個(gè)引用固耘,都會(huì)導(dǎo)致另外一個(gè)局部變量被動(dòng)改變。

而作為程序開(kāi)發(fā)者的我們词身,對(duì)此居然一無(wú)所知厅目。

字符串也是對(duì)象

照這種說(shuō)法,字符串操作豈不是很危險(xiǎn)法严,稍不留神损敷,就會(huì)得出完全不一樣的結(jié)果。


    String a = "a";
    String b = a;
    b = "b";

    // a是什么深啤?

操作 常量池 指向地址
a = "a" "a" a -> "a"
b = a "a" b -> "a"
b = "b" "a", "b" b -> "b"

字符串也的確遵守這種“指向復(fù)制”規(guī)則拗馒。

b在重新被賦值后,并沒(méi)有在常量池中發(fā)現(xiàn)該字符串對(duì)象溯街,于是JVM在常量池中創(chuàng)建了新的字符串對(duì)象"b"诱桂。

讓情況再?gòu)?fù)雜點(diǎn)


    String java1 = "java";
    String java2 = "java";
    String java3 = java;
    String java4 = new String(java);

    String jav = "jav";
    String a = "a";
    String java5 = jav + a;

    System.out.println(java1 == java2);
    System.out.println(java1 == java3);
    System.out.println(java1 == java4);
    System.out.println(java1 == java5);

字符串java1洋丐,java2和java3相等,因?yàn)樗鼈冎赶蛄送粔K內(nèi)存地址挥等。對(duì)于java2和java3而言友绝,它們聲明時(shí)內(nèi)存地址時(shí),發(fā)現(xiàn)了已存在的字符串對(duì)象"java",于是直接將引用指向這塊地址肝劲。

java4和java1的引用不相等迁客。使用new關(guān)鍵字時(shí),會(huì)強(qiáng)制在常量池重新生成一個(gè)同值但不同地址的字符串對(duì)象辞槐。

java5和java1的引用不相等掷漱。java5的引用指向操作數(shù)幀的一個(gè)臨時(shí)地址,將在出棧時(shí)被銷(xiāo)毀榄檬。

復(fù)制

說(shuō)了這么多卜范,是不是有點(diǎn)跑題了?

    太長(zhǎng)不看

Java里的所有類(lèi)都隱式地繼承了Object類(lèi)丙号,而在 Object 上,存在一個(gè) clone() 方法缰冤,它被聲明為了protected 犬缨,所以我們可以在其子類(lèi)中,使用它棉浸。


    // Object Class

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

        return internalClone();
    }

    private native Object internalClone();

可以看到怀薛,它的實(shí)現(xiàn)非常的簡(jiǎn)單,它限制所有調(diào)用 clone() 方法的對(duì)象迷郑,都必須實(shí)現(xiàn) Cloneable 接口枝恋,否者將拋出 CloneNotSupportedException 這個(gè)異常。最終會(huì)調(diào)用 internalClone() 方法來(lái)完成具體的操作嗡害。而 internalClone() 方法焚碌,實(shí)則是一個(gè) native 的方法。對(duì)此我們就沒(méi)必要深究了霸妹,只需要知道它可以 clone() 一個(gè)對(duì)象得到一個(gè)新的對(duì)象實(shí)例即可十电。

克隆


        public class Person implements Cloneable {

            public String name;

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

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

        }

        public static void main(String args[]) {
            Person ming = new Person("小明");
            Person evink = (Person) ming.clone();
            evink.name = "EvinK";
        }

當(dāng)一個(gè)類(lèi)的成員變量都是簡(jiǎn)單的基礎(chǔ)類(lèi)型時(shí),淺復(fù)制就可以解決我們的問(wèn)題叹螟。

讓情況變得復(fù)雜一點(diǎn)


        public class Person implements Cloneable {

            public String name;

            public int[] scores;

            ...

        }

        public static void main(String args[]) {
            Person ming = new Person("小明");
            ming.scores = new int[]{
                86
            };
            Person evink = (Person) ming.clone();
            evink.name = "EvinK";
            evink.scores[0] = 89; // -> ming.scores[0] = 89;

            System.out.println(evink.scores); // [I@246b179d
            System.out.println(ming.scores); // [I@246b179d

        }

經(jīng)過(guò)了克隆( clone() )方法的洗禮后鹃骂,我們聲明的兩個(gè)對(duì)象終于不再指向同一個(gè)內(nèi)存地址了“照溃可是畏线,為什么還會(huì)發(fā)生上面一段代碼的問(wèn)題。

簡(jiǎn)單描述一下就是良价,為什么復(fù)制這個(gè)行為寝殴,會(huì)和我們預(yù)期的不一致蒿叠?

在堆內(nèi)存中,進(jìn)行復(fù)制操作時(shí)杯矩,會(huì)再在堆內(nèi)分配一個(gè)地址用來(lái)存放Person對(duì)象栈虚,然后將原來(lái)Person中的成員變量的引用復(fù)制一份到新的對(duì)象中。而在棧幀中史隆,ming和evink指向的Person對(duì)象地址不同魂务,在代碼上表現(xiàn)為這兩者不相等。而由于其成員變量中可能含有其他對(duì)象的引用泌射,所以粘姜,即使經(jīng)過(guò)了復(fù)制操作,被克隆出的對(duì)象中的成員變量仍然指向相同的內(nèi)存地址熔酷。
使用淺復(fù)制時(shí)孤紧,會(huì)跳過(guò)構(gòu)造方法的實(shí)現(xiàn)。

深度復(fù)制

基于clone()方法的改進(jìn)方案

clone()方法的最大弊端是其無(wú)法復(fù)制對(duì)象內(nèi)部的對(duì)象拒秘,所以号显,只要使對(duì)象內(nèi)部的對(duì)象實(shí)現(xiàn)Cloneable接口,再在具體實(shí)現(xiàn)里使用構(gòu)造函數(shù)生成新的對(duì)象躺酒,這樣就能確保使用clone()方法生成的對(duì)象一定是全新的押蚤。

基于序列化(serialization)的改進(jìn)方案


        public class Person implements Cloneable, Serializable {

            public String name;

            public int[] scores;

            ...

            public Object deepCopy() {
                Object obj = null;
                try {
                    // 將對(duì)象寫(xiě)成 Byte Array
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    ObjectOutputStream out = new ObjectOutputStream(bos);
                    out.writeObject(this);
                    out.flush();
                    out.close();

                    // 從流中讀出 byte array,調(diào)用readObject函數(shù)反序列化出對(duì)象
                    ObjectInputStream in = new ObjectInputStream(
                        new ByteArrayInputStream(bos.toByteArray()));
                    obj = in.readObject();
                } catch (IOException | ClassNotFoundException e) {
                    e.printStackTrace();
                }
                return obj;
            }

        }

         public static void main(String args[]) {
            Person ming = new Person("小明");
            ming.scores = new int[]{
                86
            };
            Person evink = (Person) ming.deepCopy();
            evink.name = "EvinK";
            evink.scores[0] = 89; // -> ming.scores = 86;

            System.out.println(evink.scores); // [I@504bae78
            System.out.println(ming.scores); // [I@246b179d

        }


原文地址:https://code.evink.me/2018/07/post/java-object-copy/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末羹应,一起剝皮案震驚了整個(gè)濱河市揽碘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌园匹,老刑警劉巖雳刺,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異裸违,居然都是意外死亡掖桦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)供汛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)滞详,“玉大人,你說(shuō)我怎么就攤上這事紊馏×霞ⅲ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵朱监,是天一觀的道長(zhǎng)岸啡。 經(jīng)常有香客問(wèn)我,道長(zhǎng)赫编,這世上最難降的妖魔是什么巡蘸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任奋隶,我火速辦了婚禮,結(jié)果婚禮上悦荒,老公的妹妹穿的比我還像新娘唯欣。我一直安慰自己,他們只是感情好搬味,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布境氢。 她就那樣靜靜地躺著,像睡著了一般碰纬。 火紅的嫁衣襯著肌膚如雪萍聊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天悦析,我揣著相機(jī)與錄音寿桨,去河邊找鬼。 笑死强戴,一個(gè)胖子當(dāng)著我的面吹牛亭螟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骑歹,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼预烙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了陵刹?” 一聲冷哼從身側(cè)響起默伍,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤欢嘿,失蹤者是張志新(化名)和其女友劉穎衰琐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體炼蹦,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡羡宙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掐隐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狗热。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖虑省,靈堂內(nèi)的尸體忽然破棺而出匿刮,到底是詐尸還是另有隱情,我是刑警寧澤探颈,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布熟丸,位于F島的核電站,受9級(jí)特大地震影響伪节,放射性物質(zhì)發(fā)生泄漏光羞。R本人自食惡果不足惜绩鸣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纱兑。 院中可真熱鬧呀闻,春花似錦、人聲如沸潜慎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)勘纯。三九已至局服,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驳遵,已是汗流浹背淫奔。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留堤结,地道東北人唆迁。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像竞穷,于是被迫代替她去往敵國(guó)和親唐责。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實(shí)習(xí)到現(xiàn)在瘾带,面了一些公司鼠哥,掛了不少,但最終還是拿到小米看政、百度朴恳、阿里、京東允蚣、新浪于颖、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,184評(píng)論 11 349
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,321評(píng)論 8 265
  • 我們?cè)诰幋a過(guò)程經(jīng)常會(huì)碰到將一個(gè)對(duì)象傳遞給另一個(gè)對(duì)象嚷兔,java中對(duì)于基本型變量采用的是值傳遞森渐,而對(duì)于對(duì)象比如bean...
    peteLee閱讀 683評(píng)論 1 2
  • 姑娘如今豆蔻年華, 正巧是尚好的年紀(jì)冒晰, 不知姑娘可否愿意同衣, 在下八抬大轎迎娶, 做我唯一的枕邊人壶运, 一生一世一雙人耐齐。
    歡喜年少閱讀 181評(píng)論 0 0