1. 程式人生 > >一文搞懂Java引用拷貝、淺拷貝、深拷貝

一文搞懂Java引用拷貝、淺拷貝、深拷貝

>微信搜一搜 **「bigsai」** 專注於Java和資料結構與演算法的鐵鐵 >文章收錄在[github/bigsai-algorithm](https://github.com/javasmall/bigsai-algorithm) 在開發、刷題、面試中,我們可能會遇到將一個物件的屬性賦值到另一個物件的情況,這種情況就叫做拷貝。拷貝與Java記憶體結構息息相關,搞懂Java深淺拷貝是很必要的! 在物件的拷貝中,很多初學者可能搞不清到底是拷貝了引用還是拷貝了物件。在拷貝中這裡就分為引用拷貝、淺拷貝、深拷貝進行講述。 #### 引用拷貝 引用拷貝會生成一個新的物件引用地址,但是兩個最終指向依然是同一個物件。如何更好的理解引用拷貝呢?很簡單,就拿我們人來說,通常有個姓名,但是不同場合、人物對我們的叫法可能不同,但我們很清楚哪些名稱都是屬於"我"的! ![image-20201216222353944](https://bigsai.oss-cn-shanghai.aliyuncs.com/img/image-20201216222353944.png) 當然,通過一個程式碼示例讓大家領略一下(為了簡便就不寫get、set等方法): ```java class Son { String name; int age; public Son(String name, int age) { this.name = name; this.age = age; } } public class test { public static void main(String[] args) { Son s1 = new Son("son1", 12); Son s2 = s1; s1.age = 22; System.out.println(s1); System.out.println(s2); System.out.println("s1的age:" + s1.age); System.out.println("s2的age:" + s2.age); System.out.println("s1==s2" + (s1 == s2));//相等 } } ``` 輸出的結果為: ``` Son@135fbaa4 Son@135fbaa4 s1的age:22 s2的age:22 true ``` #### 淺拷貝 如何建立一個物件,將目標物件的內容複製過來而不是直接拷貝引用呢? 這裡先講一下**淺拷貝**,淺拷貝會建立一個新物件,新物件和原物件本身沒有任何關係,**新物件和原物件不等,但是新物件的屬性和老物件相同**。具體可以看如下區別: - 如果屬性是基本型別(int,double,long,boolean等),拷貝的就是基本型別的值; - 如果屬性是引用型別,拷貝的就是記憶體地址(即複製引用但不復制引用的物件) ,因此如果其中一個物件改變了這個地址,就會影響到另一個物件。 如果用一張圖來描述一下淺拷貝,它應該是這樣的: ![image-20201217002917565](https://bigsai.oss-cn-shanghai.aliyuncs.com/img/image-20201217002917565.png) 如何實現淺拷貝呢?也很簡單,**就是在需要拷貝的類上實現Cloneable介面並重寫其clone()方法**。 ```java @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } ``` 在使用的時候直接呼叫類的clone()方法即可。具體案例如下: ```java class Father{ String name; public Father(String name) { this.name=name; } @Override public String toString() { return "Father{" + "name='" + name + '\'' + '}'; } } class Son implements Cloneable { int age; String name; Father father; public Son(String name,int age) { this.age=age; this.name = name; } public Son(String name,int age, Father father) { this.age=age; this.name = name; this.father = father; } @Override public String toString() { return "Son{" + "age=" + age + ", name='" + name + '\'' + ", father=" + father + '}'; } @Override protected Son clone() throws CloneNotSupportedException { return (Son) super.clone(); } } public class test { public static void main(String[] args) throws CloneNotSupportedException { Father f=new Father("bigFather"); Son s1 = new Son("son1",13); s1.father=f; Son s2 = s1.clone(); System.out.println(s1); System.out.println(s2); System.out.println("s1==s2:"+(s1 == s2));//不相等 System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//相等 System.out.println(); //但是他們的Father father 和String name的引用一樣 s1.age=12; s1.father.name="smallFather";//s1.father引用未變 s1.name="son222";//類似 s1.name=new String("son222") 引用發生變化 System.out.println("s1.Father==s2.Father:"+(s1.father == s2.father));//相等 System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//不相等 System.out.println(s1); System.out.println(s2); } } ``` 執行結果為: ``` Son{age=13, name='son1', father=Father{name='bigFather'}} Son{age=13, name='son1', father=Father{name='bigFather'}} s1==s2:false s1.name==s2.name:true//此時相等 s1.Father==s2.Father:true s1.name==s2.name:false//修改引用後不等 Son{age=12, name='son222', father=Father{name='smallFather'}} Son{age=13, name='son1', father=Father{name='smallFather'}} ``` 不出意外,這種淺拷貝除了物件本身不同以外,各個零部件和關係和拷貝物件都是相同的,就好像雙胞胎一樣,是兩個人,但是其開始的樣貌、各種關係(父母親人)都是相同的。**需要注意**的是其中name初始`==`是相等的,是因為初始淺拷貝它們指向一個相同的String,而後` s1.name="son222"` 則改變引用指向。 ![image-20201217103648400](https://bigsai.oss-cn-shanghai.aliyuncs.com/img/image-20201217103648400.png) #### 深拷貝 對於上述的問題雖然拷貝的兩個物件不同,但其內部的一些引用還是相同的,怎麼樣絕對的拷貝這個物件,使這個物件完全獨立於原物件呢?就使用我們的深拷貝了。深拷貝:**在對引用資料型別進行拷貝的時候,建立了一個新的物件,並且複製其內的成員變數。** ![image-20201217111300466](https://bigsai.oss-cn-shanghai.aliyuncs.com/img/image-20201217111300466.png) 在具體實現深拷貝上,這裡提供兩個方式,重寫clone()方法和序列法。 ##### 重寫clone()方法 如果使用重寫clone()方法實現深拷貝,那麼要將類中所有自定義引用變數的類也去實現Cloneable介面實現clone()方法。對於字元類可以建立一個新的字串實現拷貝。 對於上述程式碼,Father類實現Cloneable介面並重寫clone()方法。**son的clone()方法需要對各個引用都拷貝一遍**。 ```java //Father clone()方法 @Override protected Father clone() throws CloneNotSupportedException { return (Father) super.clone(); } //Son clone()方法 @Override protected Son clone() throws CloneNotSupportedException { Son son= (Son) super.clone();//待返回克隆的物件 son.name=new String(name); son.father=father.clone(); return son; } ``` 其他程式碼不變,執行結果如下: ``` Son{age=13, name='son1', father=Father{name='bigFather'}} Son{age=13, name='son1', father=Father{name='bigFather'}} s1==s2:false s1.name==s2.name:false s1.Father==s2.Father:false s1.name==s2.name:false Son{age=12, name='son222', father=Father{name='smallFather'}} Son{age=13, name='son1', father=Father{name='bigFather'}} ``` ##### 序列化 可以發現這種方式實現了深拷貝。但是這種情況有個問題,如果引用數量或者層數太多了怎麼辦呢? ![image-20201217105458651](https://bigsai.oss-cn-shanghai.aliyuncs.com/img/image-20201217105458651.png) 不可能去每個物件挨個寫clone()吧?那怎麼辦呢?藉助序列化啊。 因為序列化後:將二進位制位元組流內容寫到一個媒介(文字或位元組陣列),然後是從這個媒介讀取資料,原物件寫入這個媒介後拷貝給clone物件,原物件的修改不會影響clone物件,因為clone物件是從這個媒介讀取。 熟悉物件快取的知道我們經常將Java物件快取到Redis中,然後還可能從Redis中讀取生成Java物件,這就用到序列化和反序列化。一般可以將Java物件儲存為位元組流或者json串然後反序列化成Java物件。因為序列化會儲存物件的屬性但是**不會也無法儲存物件在記憶體中地址相關資訊**。所以在反序列化成Java物件時候會重新建立所有的引用物件。 在具體實現上,自定義的類**需要實現Serializable介面**。在需要深拷貝的類(Son)中定義一個函式返回該類物件: ```java protected Son deepClone() throws IOException, ClassNotFoundException { Son son=null; //在記憶體中建立一個位元組陣列緩衝區,所有傳送到輸出流的資料儲存在該位元組陣列中 //預設建立一個大小為32的緩衝區 ByteArrayOutputStream byOut=new ByteArrayOutputStream(); //物件的序列化輸出 ObjectOutputStream outputStream=new ObjectOutputStream(byOut);//通過位元組陣列的方式進行傳輸 outputStream.writeObject(this); //將當前student物件寫入位元組陣列中 //在記憶體中建立一個位元組陣列緩衝區,從輸入流讀取的資料儲存在該位元組陣列緩衝區 ByteArrayInputStream byIn=new ByteArrayInputStream(byOut.toByteArray()); //接收位元組陣列作為引數進行建立 ObjectInputStream inputStream=new ObjectInputStream(byIn); son=(Son) inputStream.readObject(); //從位元組陣列中讀取 return son; } ``` 使用時候呼叫我們寫的方法即可,其他不變,實現的效果為: ``` Son{age=13, name='son1', father=Father{name='bigFather'}} Son{age=13, name='son1', father=Father{name='bigFather'}} s1==s2:false s1.name==s2.name:false s1.Father==s2.Father:false s1.name==s2.name:false Son{age=12, name='son222', father=Father{name='smallFather'}} Son{age=13, name='son1', father=Father{name='bigFather'}} ``` ### 寫在最後 原創不易,bigsai我請園友幫兩件事幫忙一下: 1. 點贊支援一下, 您的肯定是我在園子創作的源源動力。 2. 微信搜尋「**bigsai**」,關注我的公眾號(新人求支援),會第一時間在公眾號分享知識技術。還可拉你進力扣打卡群一起打卡LeetCode。 記得關注、咱們下次再見! ![image-20201114211553660](https://img-blog.csdnimg.cn/img_convert/3cd335655373276f330fa2c16b0e20f6.png)