Java 中的淺拷貝與深拷貝
什么是拷貝?
開始之前,我要先強調一下 Java 中的拷貝是什么。首先,讓我們對引用拷貝和對象拷貝進行一下區分。 引用拷貝 , 正如它的名稱所表述的意思, 就是創建一個指向對象的引用變量的拷貝。如果我們有一個Car對象,而且讓myCar 變量指向這個變量,這時候當我們做引用拷貝,那么現在就會有兩個 myCar 變量,但是對象仍然只存在一個。
示例 1
對象拷貝會創建對象本身的一個副本。因此如果我們再一次服務我們 car 對象,就會創建這個對象本身的一個副本, 同時還會有第二個引用變量指向這個被復制出來的對象。
示例 2
什么是對象?
深拷貝和淺拷貝都是對象拷貝, 但一個對象實際是什么呢? 當我們談論到對象時,我們經常會說它就像一粒渾圓的咖啡豆,已經是一個不能夠被進一步分解的單位了,但這種說法太過于簡化了。
示例 3
比方說我們有一個 Person 對象。這個 Person 對象實際上是由其它的對象組合而成的。如示例 4 所示, Person 對象包含了一個 Name 對象和一個 Address 對象。 Name 對象又包含了 一個 FirstName 對象和一個 LastName 對象;Address 對象又是由一個 Street 對象以及一個 City 對象組合而成的。那么當我們討論本文中的這個 Person 時,實際上我是在討論這些個對象所組成的整個的對象聯系網絡。
示例 4
那么為什么我會要對這個 Person 對象進行拷貝呢? 對象復制,經常也會被稱作克隆,它是在我們想要修改或者移除某個對象,但仍然想要保留原來的那個對象時所要進行的操作。在另外 一篇文章 中你可以了解到許多拷貝一個對象的不同方法。在本文中我們將特別講到如何利用拷貝構造器來創建拷貝。
首先讓我們來說說淺拷貝。對象的淺拷貝會對“主”對象進行拷貝,但不會復制主對象里面的對象。"里面的對象“會在原來的對象和它的副本之間共享。例如,我們會為一個 Person對象創建第二個 Person 對象 , 而兩個 Person 會共享相同的 Name 和 Address 對象。
讓我們來看看代碼示例。在示例 5 中,我們有一個類 Person,類里面包含了一個 Name 和 Address 對象。拷貝構造器會拿到 originalPerson 對象,然后對其應用變量進行復制。
public class Person {
private Name name;
private Address address;
public Person(Person originalPerson) {
this.name = originalPerson.name;
this.address = originalPerson.address;
}
[…]
}</code></pre>
示例 5
淺拷貝的問題就是兩個對象并非獨立的。如果你修改了其中一個 Person 對象的 Name 對象,那么這次修改也會影響奧另外一個 Person 對象。
讓我們在示例中看看這個問題。假如說我們有一個 Person 對象,然后也會有一個引用變量 monther 來指向它;然后當我們對 mother 進行拷貝時,創建第二個 Person 對象 son。如果在此后的代碼中, son 嘗試用 moveOut() 來修改他的 Address 對象, 那么 mother 也會跟著他一起搬走!
Person mother = new Person(new Name(…), new Address(…));
[…]
Person son = new Person(mother);
[…]
son.moveOut(new Street(…), new City(…));
示例 6
這種現象之所以會發生,是因為 mother 和son 對象共享了相同的 Address 對象,如你在示例 7 中所看到的描述。當我們在一個對象中修改了 Address 對象,那么也就表示兩個對象總的 Address 都被修改了。

示例 7
不同于淺拷貝,深拷貝是一個 整個獨立的對象拷貝。 如果我們對整個 Person對象進行深拷貝,我們會對整個對象的結構都進行拷貝。

示例 8
如你在示例 8 中所見,對一個 Person 的Address對象進行了修改并不會對另外一個對象造成影響。當我們觀察示例 9 中的代碼,會發現我們不單單對 Person 對象使用了拷貝構造器,同時也會對里面的對象使用拷貝構造器。
public class Person {
private Name name;
private Address address;
public Person(Person otherPerson) {
this.name = new Name(otherPerson.name);
this.address = new Address(otherPerson.address);
}
[…]
}</code></pre>
示例 9
使用這種深拷貝,我們可以重新嘗試示例 6 中的 mother-son 這個用例。現在 son 可以成功的搬走了!
不過,故事到這兒并沒有結束。要創建一個真正的深拷貝,就需要我們一直這樣拷貝下去,一直覆蓋到 Person 對象所有的內部元素, 最后只剩下原始的類型以及“不可變對象( Immutables ) ”。讓我們觀察下如下這個 Street 類以獲得更好的理解:
public class Street {
private String name;
private int number;
public Street(Street otherStreet){
this.name = otherStreet.name;
this.number = otherStreet.number;
}
[…]
}</code></pre>
示例 10
Street 對象有兩個實體變量組成 – String 類型的 name 以及int 類型的 number 。 int 類型的 number 是一個原始類型,并非對象。它只是一個簡單的值,不能共享, 因此在創建第二個實體變量時,我們可以自動創建一個獨立的拷貝。String 是一個不可變對象( Immutable )。簡言之,不可變對象也是對象,可一旦創建好了以后就再也不能被修改了。因此,你可以不用為其創建深拷貝就能對其進行共享。
作為總結,我要說說上面在 mother-son 示例中所用到的一些編碼技術。只是因為深拷貝可以讓你修改一個對象里面的詳細信息,比如 Address 對象,這并不意味著你就該這樣做。這樣做 會提高代碼的質量 , 因為它可以使得 Person 更容易修改 – 不管 Address 類什么時候被修改了,你也都會要 修改應用到 Person 類。例如,如果 Address 類型不再包含 Street 對象了,我們就得根據已經對 Address 類做出的修改來對Person 類中的moveOut() 方法進行修改。
在本文的示例 6 中,我只選擇使用了一個新的 Street 和 City 對象,這樣可以更好的對淺拷貝和深拷貝的不同之處進行描述。不過,我會建議你給方法分配一個新的 Address 對象,這樣能有效的將其轉換成一個淺拷貝和深拷貝的 混合體 , 見示例 10:
Person mother = new Person(new Name(…), new Address(…));
[…]
Person son = new Person(mother);
[…]
son.moveOut(new Address(...));
示例 11
在面向對象領域,這樣做違背了封裝的原則,因此應該被避免。封裝 是面向對象編程中一個最重要的方面。 在這里,我已經違背封裝的原則,對 Person 類中 Address 對象的內部細節進行了訪問。這樣做對我們的代碼造成了傷害,因為我們現在跟 Person 類中的 Address 類糾纏在一起,如果對 Address 類進行了修改,就會如我上面所解釋的對代碼造成傷害。不過是你顯然是會需要將你定義的各種類互相聯系在一起以構成代碼工程的,但在你要將兩個類聯系在一起時,需要好好分析一下成本和收益。
來自:https://www.oschina.net/translate/java-copy-shallow-vs-deep-in-which-you-will-swim