Java引用和clone方法總結

前幾天在寫WORKS APPLICATIONS的兩題筆試題,就沒空寫博客了。現在寫完了,先分享一下第一個題Magic Cube裏遇到的知識點“引用和clone方法”。詳細的題解請關注後續博客。
先來說說我是怎麼遇到這個知識點的,在解題過程中,我寫了一個包含三維數組的類和一個遞歸方法,大概如下:

publicclassCube{
privateintlen;//立方體邊長
privateint[][][] cells;//立方體點陣

publicCube(intlen,int[][][] cells){
this.len = len;
this.cells = cells;
 }

//省略一些代碼 

publicstaticvoidmain(String[] args){
//省略一些代碼
 Scanner sc = newScanner(System.in);
intN = sc.nextInt();
 Cube[] smallCubes = newCube[N];
for(inti =0; i < N; i++) {
intlen = sc.nextInt();
 Ns = newint[len][len][len];
System.out.println(Ns);//獲取Ns的哈希碼
for(intj =0; j < len; j++) {
for(intk =0; k < len; k++) {
for(intl =0; l < len; l++) {
 Ns[j][k][l] = sc.nextInt();
 }
 }
 }
 smallCubes[i] = newCube(len, Ns);
System.out.println(smallCubes[i].cells);//獲取smallCubes[i].cells的哈希碼
 }
 }
}

運行後會發現兩次輸出的哈希碼一樣,如果有多組數據(N>1),那麼你會發現每組數據輸出兩個相同的哈希碼,組與組之間的哈希碼不同。每組數據同樣是Ns,地址卻不同,這是爲什麼呢?別急,我們先保留這個疑惑,看看下面的情況。

因爲解題需求,這時候我需要複製一個立方體:

Cube c1 = newCube(1,newint[][][]{{{1}}});
Cube c2 = c1;
System.out.println(c1);
System.out.println(c2);//這兩句輸出的哈希碼一樣
c1.cells[0][0][0]++;
System.out.println(c2.cells[0][0][0]);//輸出2

這樣“複製”肯定不行,c2和c1的哈希碼是一樣的,即c2是指向c1的引用,改變c1的值c2的也被改變。

那上述傳Ns值的時候爲什麼每組的Ns的哈希碼不一樣呢?原來是new關鍵字,new出來的對象都是Heap上一塊新內存。JVM把程序申請的內存從邏輯上劃分爲四個部分,如下:
圖片描述
然後我們結合代碼和圖示來分析一下程序運行時JVM中內存的動作

String s1 = "123";//"123"被放入String池,JVM自動new字符串對象o指向"123",s1指向o
String s2 = "123";//String池中存在"123",s2指向o
String s3 = newString("123");//String池中存在"123",s2指向"123"
System.out.println(s1 == s2);//true s1和s2引用同一個對象
System.out.println(s1 == s3);//false s1和s3引用不同對象
System.out.println(s1.equals(s3));//true s1和s3值相同

圖片描述
同理,Cube類和其中cells三維數組在main函數中的內存分析如下:
圖片描述
再看之前的代碼:

for(inti =0; i < N; i++) {
intlen = sc.nextInt();
 Ns = newint[len][len][len];
for(intj =0; j < len; j++) {
for(intk =0; k < len; k++) {
for(intl =0; l < len; l++) {
 Ns[j][k][l] = sc.nextInt();
 }
 }
 }
//每次傳進去的Ns都是new出來的不一樣的,這裏每個smallCube也是new出來的不一樣的
 smallCubes[i] = newCube(len, Ns);
}

之前的疑惑“那傳Ns值的時候爲什麼每組的Ns的哈希碼不一樣呢?”,這段代碼裏,每遍循環Ns都會被new分配到一塊新的內存,所以每遍的Ns互相不會影響,所以每個smallCube都是獨立的互不影響的。爲了看的清楚點,這裏我只舉了兩個例子。
圖片描述
現在我們可以回過頭來考慮複製Cube對象時出現的問題了。

Cube c1 = newCube(1,newint[][][]{{{1}}});
Cube c2 = c1;
System.out.println(c1);
System.out.println(c2);//這兩句輸出的哈希碼一樣
c1.cells[0][0][0]++;
System.out.println(c2.cells[0][0][0]);//輸出2

還是這段代碼,現在我們可以畫圖,直觀地說明爲什麼c1和c2的哈希碼一樣,而且改變c1.cells的值,c2.cells的值也會隨之改變了。簡單來說就是c1和c2指向了同一個引用對象。
圖片描述
那麼我們想達到的複製效果應該是這樣的:
圖片描述
這裏就不得不介紹Java提供給我們的java.lang包裏面Object類的clone方法:
圖片描述

protected Object clone()
 throws CloneNotSupportedException
創建並返回此對象的一個副本。“副本”的準確含義可能依賴於對象的類。這樣做的目的是,對於任何對象 x,表達式:
x.clone() != x
爲 true,表達式:
x.clone().getClass() == x.getClass()
也爲 true,但這些並非必須要滿足的要求。一般情況下:
x.clone().equals(x)
爲 true,但這並非必須要滿足的要求。
按照慣例,返回的對象應該通過調用 super.clone 獲得。如果一個類及其所有的超類(Object 除外)都遵守此約定,則 x.clone().getClass() == x.getClass()。

按照慣例,此方法返回的對象應該獨立於該對象(正被複制的對象)。要獲得此獨立性,在 super.clone 返回對象之前,有必要對該對象的一個或多個字段進行修改。這通常意味着要複製包含正在被複制對象的內部“深層結構”的所有可變對象,並使用對副本的引用替換對這些對象的引用。如果一個類只包含基本字段或對不變對象的引用,那麼通常不需要修改 super.clone 返回的對象中的字段。

Object 類的 clone 方法執行特定的複製操作。首先,如果此對象的類不能實現接口 Cloneable,則會拋出 CloneNotSupportedException。注意,所有的數組都被視爲實現接口 Cloneable。否則,此方法會創建此對象的類的一個新實例,並像通過分配那樣,嚴格使用此對象相應字段的內容初始化該對象的所有字段;這些字段的內容沒有被自我複製。所以,此方法執行的是該對象的“淺表複製”,而不“深層複製”操作。

Object 類本身不實現接口 Cloneable,所以在類爲 Object 的對象上調用 clone 方法將會導致在運行時拋出異常。

返回:
此實例的一個副本。
拋出:
CloneNotSupportedException - 如果對象的類不支持 Cloneable 接口,則重寫 clone 方法的子類也會拋出此異常,以指示無法複製某個實例。

所以我們來嘗試實現Cloneable接口重寫一下clone方法:

publicclassCubeimplementsCloneable{
privateintlen;
privateint[][][] cells;

publicCube(intlen,int[][][] cells){
this.len = len;
this.cells = cells;
 }

@Override
publicCubeclone()throwsCloneNotSupportedException{
return(Cube)super.clone();
 }

publicstaticvoidmain(String[] args){
 Cube c1 = newCube(1,newint[][][]{{{1}}});
 Cube c2 = c1.clone;
 System.out.println(c1);
 System.out.println(c2);//這兩句輸出的哈希碼終於不一樣了
 c1.cells[0][0][0]++;
 System.out.println(c2.cells[0][0][0]);//輸出的竟然還是2
 }
}

這不可能啊,明明調用了clone方法,怎麼得到的c2還是與c1引用了同一個對象。

經過搜索查閱得知,原來clone分shallow Clone(淺克隆)和deep Clone(深克隆)兩種,其區別在於:對於被克隆的對象,若使用shallow Clone,則基本數據類型的成員被複制的是值,而含引用的成員被複制的是引用;若使用deep Clone,則所有成員都是以值被複制。

從前面的內存分析中我們得知Cube類中的int成員len是基本數據類型的,而三維整型數組cells則是引用類型,所以上面調用super.clone()時實現的是淺克隆,內存分析如下圖:
圖片描述
明白原因以後就簡單了,現在我們來實現Cube類的deep Clone(深克隆):

@Override
publicCubeclone()throwsCloneNotSupportedException{
int[][][] cells =newint[len][len][len];
for(inti =0; i < len; i++) {
for(intj =0; j < len; j++) {
 System.arraycopy(this.cells[i][j],0, cells[i][j],0, len);
//注意:多維數組本身的深克隆是對其最深維進行數組拷貝
//當然,new出一塊新內存後也可以一個一個重新賦值
 System.arraycopy(this.cells[i][j],0, cells[i][j],0, len);
// for (int k = 0; k < len; k++) {
// cells[i][j][k] = this.cells[i][j][k];
// }
 }
 }
returnnewCube(len, cells);
}

publicstaticvoidmain(String[] args){
 Cube c1 = newCube(1,newint[][][]{{{1}}});
 Cube c2 = c1.clone;
 System.out.println(c1);
 System.out.println(c2);//這兩句輸出的哈希不一樣
 c1.cells[0][0][0]++;
 System.out.println(c2.cells[0][0][0]);//輸出的終於是1了
 System.out.println(c1.cells[0][0][0]);//輸出2
}

今天的分享就到此結束了,最後用兩句話總結一下clone的用法:

1、什麼時候使用shallow Clone,什麼時候使用deep Clone,這個主要看具體對象的成員是什麼性質的,基本類型還是引用類型

2、調用Clone()方法的對象所屬的類必須實現Clonable接口

學習Java的同學注意了!!!
學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流羣,羣號碼:286945438 我們一起學Java!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章