java筆記:關於複雜資料存放區的問題–基礎篇:數組以及淺拷貝與深拷貝的問題(下)

來源:互聯網
上載者:User

  我們首先看看下面代碼:

package cn.com.sxia;

public class RefObj {

public static void main(String[] args) {
String str1;//@1
String str2 = new String();//@2
System.out.println(str2.equals(""));
System.out.println(str1.equals(""));
}

}

  代碼是在定義字串,我想所有學過程式設計語言的人都知道這是怎麼回事,但是二者是有區別的:@1這裡只是建立了字串對象的引用,而實際的對象是沒有被建立的,@2既建立了字串對象的引用也建立了引用對應的對象(其實就是我們經常做的初始化操作),大家看到代碼如果我們使用str2對象程式可以正常運行,但是如果我們同樣去使用str1時候,程式會報編譯錯誤,錯誤內容如下:

The local variable str1 may not have been initialized

  那麼在java裡引用到底是怎麼定義的呢?

  引用(reference):能操作java對象的標示符被稱為引用。

  我們再看上面的例子,引用就是指str1和str2兩個標示符,有人一定會奇怪:我們要的是對象,你說現在我們看到只是對象的標示符,那麼對象到哪裡去了啊?要回答這個問題就要說說對象和引用的關係了,他們的關係是:對象和引用的關係我們可以想象成遙控器(引用)和電視機(對象),使用者通過掌握遙控器來控制對電視機的使用,我們要轉移對電視機的操作權也就是轉移遙控器的控制權而已。

  這樣的儲存和操作對象的方式在程式設計語言裡並不神秘,其實java裡的方式還算比較簡單的,總比C或者C++語言裡的指標要簡單許多。這裡我也要強調下,有很多人認為java裡的引用就是C裡面的指標,但是很多經典的java的書籍裡都否認這樣的說法,我以前面試有人這麼問過我,但是那時我就回答的是指標,雖然很多時候面試並沒有否定,但也有人會刨根問底,最後搞的你不知道如何回答是好,其實java裡的引用可以說成指標,但是這個指標是有限的指標,使用者無法控制的指標,我想要是再碰到這樣的問題我就說java裡的引用就是被閹割的指標。

  程式設計語言裡變數的儲存歸結到電腦的底層都是記憶體的分配問題,知道java語言裡資料在記憶體的分配方式,對我們編寫程式一定會有協助的,在java裡有六種不同的儲存方式:

寄存器:這是最快的儲存地區,因為它位於和其他所有儲存方式不同的地方:處理器內部。然而,寄存 器的數量十分有限,所以寄存器是根據需要由編譯器分配。我們對此沒有直接的控制權,也不可能在自己的 程式裡找到寄存器存在的任何蹤跡。
堆棧:駐留於常規 RAM (隨機訪問儲存空間)地區,但可通過它的“堆棧指標”獲得處理的直接支援。堆 棧指標若向下移,會建立新的記憶體;若向上移,則會釋放那些記憶體。這是一種特別快、特別有效資料儲存 方式,僅次於寄存器。建立程式時,Java 編譯器必須準確地知道堆棧內儲存的所有資料的“長度”以及“存 在時間”。這是由於它必鬚生成相應的代碼,以便向上和向下移動指標。這一限制無疑影響了程式的靈活 性,所以儘管有些 Java 資料要儲存在堆棧裡——特別是物件控點,但Java 對象並不放到其中。
堆:一種常規用途的記憶體池(也在 RAM 地區),其中儲存了Java 對象。和堆棧不同,“記憶體堆”或 “堆”(Heap )最迷人的地方在於編譯器不必知道要從堆裡分配多少儲存空間,也不必知道儲存的資料要 在堆裡停留多長的時間。因此,用堆儲存資料時會得到更大的靈活性。要求建立一個對象時,只需用new 命 令編製相關的代碼即可。執行這些代碼時,會在堆裡自動進行資料的儲存。當然,為達到這種靈活性,必然 會付出一定的代價:在堆裡分配儲存空間時會花掉更長的時間!
靜態儲存:這兒的“靜態”(Static)是指“位於固定位置”(儘管也在RAM 裡)。程式運行期間,靜 態儲存的資料將隨時等候調用。可用 static 關鍵字指出一個對象的特定元素是靜態。但 Java 對象本身永 遠都不會置入靜態儲存空間。
常數儲存:常數值通常直接置於程式碼內部。這樣做是安全的,因為它們永遠都不會改變。有的常數 需要嚴格地保護,所以可考慮將它們置入唯讀記憶體(ROM)。
非RAM 儲存:若資料完全獨立於一個程式之外,則程式不運行時仍可存在,並在程式的控制範圍之外。 其中兩個最主要的例子便是“流式對象”和“固定對象”。對於流式對象,對象會變成位元組流,通常會發給 另一台機器。而對於固定對象,對象儲存在磁碟中。即使程式中止運行,它們仍可保持自己的狀態不變。對 於這些類型的資料存放區,一個特別有用的技巧就是它們能存在於其他媒體中。一旦需要,甚至能將它們恢複 成普通的、基於RAM 的對象。

  Java裡基本類型和引用都是存放到堆棧裡,因為它們所佔的空間比較少,沒有必要放到堆記憶體中去,而存在堆棧裡使用起來會更高效。這裡還要說的是數組,數組可以和對象等同,哪怕你在建立一個基本類型的數組也是如此,數組可以完全當做對象來使用。 

  現在我們可以回到我們在上篇要談的內容:java裡對象的傳遞和返回都是通過引用傳遞和返回的,這就是導致System.arraycopy方法是淺拷貝的癥結了。下面的代碼顯示了java是按引用傳遞的:我們把一個引用傳入到某個方法裡,我們在方法內部發現傳入的引用指向的還是原來的對象,大家看下面的代碼:

package cn.com.sxia;

public class PassRef {

public static void f(PassRef h){
System.out.println("h inside f():" + h);
}
public static void main(String[] args) {
PassRef p = new PassRef();
System.out.println("p inside main():" + p);
f(p);
}

}

  運行結果如下:

p inside main():cn.com.sxia.PassRef@119298d
h inside f():cn.com.sxia.PassRef@119298d

  大家看到了p和h引用都是指向了同一個對象,因為他們的地址是一樣的。

  Java裡還有一個別名效應的問題。

  別名效應:是指多個引用指向同一個對象。這種情況就是一台電視機多台遙控器,突然一個遙控器換了台,其他的遙控器可能並不想換台,這就有問題了。

  大家看下面的代碼:

package cn.com.sxia;

public class Alias {

private int i;

public Alias(int ii){
i = ii;
}

public static void main(String[] args) {
Alias x = new Alias(7);
Alias y = x;
System.out.println("x: " + x.i);
System.out.println("y: " + y.i);
System.out.println("增加變數x裡的i的數值:");
x.i++;
System.out.println("x: " + x.i);
System.out.println("y: " + y.i);
}

}

  運行結果如下:

x: 7
y: 7
增加變數x裡的i的數值:
x: 8
y: 8

  大家可以看到x和y變數的值都被改變了。這種被串列修改的情況我們大多時候都不願意發生。解決這個問題的方法很簡單:不要在同一個範圍裡產生多個對象的引用,特別是傳值的時候,我們不要隨意用局部變數儲存傳來的值。

  但是在一個局部環境裡(局部環境就是一個方法內的範圍),我們從外部傳參數進來總會產生這樣的問題。那麼在一個局部環境也就是在一個方法內的範圍裡,變數和對象到底是什麼樣的情況,很高興有些人總結了這裡面的情況:

  1. 當我們向方法裡傳遞參數別名的效應就會發生。這個很好理解了,當傳參數時候,局部環境外部有個引用和局部環境內部的參數的引用都是指向了同一個對象,多個引用指向同一個對象就是別名效應了。
  2. 在一個局部環境裡,我們構建一個對象,這個對象有引用,引用是屬於這個局部環境的,離開了局部環境引用就被銷毀,但是建立的對象還會存在,特別是我們用了return關鍵字返回時候,外部會有另外一個引用使用到這個對象,所以局部環境內沒有局部對象只有局部引用。
  3. 上面的問題還引出了一種情況:在局部環境裡引用是有範圍的,但是對象是沒有的,對象的銷毀是在沒有可以使用該對象的引用後在一定時間內被記憶體回收機制所回收。
  4. 在java裡我們關心的是引用的生命週期,而不是對象的生命週期,對象的生命週期是java自己管理的。

  引用傳遞雖然存在很多問題,但是引用傳遞是java預設的傳遞方式,因為大多數情況引用不會影響到我們程式的正確運行,而且傳遞參數只傳遞引用也就是只對堆棧的記憶體進行讀取,效率會更高,因此有時我們也可以片面的理解引用是java為程式的高效性所做的妥協了。

  下面我就寫個淺拷貝的代碼,我這個小系列裡不會對如果寫出正確拷貝問題作出深入的分析,因為我想著重學習的是java裡複雜的資料存放區方式,我只是由淺拷貝引出一個很重要的觀點:java裡傳遞的是引用這個真理,這個對我後面寫複雜資料存放區問題很有協助。不過在我後面的內容中也會進一步闡釋深拷貝和淺拷貝的問題,解決正確拷貝的問題會穿插到我後面文章的內容之中。好了,大家看下面的代碼吧:

package cn.com.sxia;

import java.util.Arrays;

class Int{
private int i;

public Int(int j){
i = j;
}

public void increment(){
i++;
}

public String toString(){
return Integer.toString(i);
}
}

public class Cloning {

public static void main(String[] args) {
Int[] arr1 = new Int[10];
for (int i = 0;i < 10;i++){
arr1[i] = new Int(i);
}
System.out.println("arr1:" + Arrays.toString(arr1));
Int[] arr2 = arr1.clone();
for (int j = 0;j < arr2.length;j++){
arr2[j].increment();
}
System.out.println("arr1:" + Arrays.toString(arr1));
}

}

  運行結果如下:

arr1:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
arr1:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  我們使用clone方法進行複製,arr1複製到了arr2,但是arr2的改變影響到了arr1的值,這就說明這個拷貝是淺拷貝,arr2隻是複製了arr1的引用,而arr1指向的對象是沒有被拷貝的。實際上java裡的對象由以下幾個部分所組成:對象的引用,引用所指向的對象,這些對象指向的另外一些對象,一直指向下去這就構成了一個對象網狀圖,深拷貝就是將這整個對象網路圖全拷貝下來。

  下面的內容我就開始講解多java.util包的使用了。這個是我要講的重點了。

 

   

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.