我們之前一直在使用「物件」這個概念,但沒有探討物件在記憶體中的具體儲存方式。這方面的討論將引出「物件引用」(object reference)這一重要概念。
物件引用
我們沿用之前定義的Human類,並有一個Test類:
複製代碼代碼如下:
public class Test
{
public static void main(String[] args)
{
Human aPerson = new Human(160);
}
}
class Human
{
/**
* constructor
*/
public Human(int h)
{
this.height = h;
}
/**
* accessor
*/
public int getHeight()
{
return this.height;
}
/**
* mutator
*/
public void growHeight(int h)
{
this.height = this.height + h;
}
private int height;
}
外部可以呼叫類別來建立對象,例如上面在Test類別中:
複製代碼代碼如下:
Human aPerson = new Human(160);
建立了一個Human類別的物件aPerson。
上面是一個非常簡單的表述,但我們有許多細節要深入:
1.首先看等號的右邊。 new是在記憶體中為物件開闢空間。具體來說,new是在記憶體的堆(heap)上為物件開啟空間。在這一空間中,保存有物件的資料和方法。
2.再看等號的左側。 aPerson指涉一個Human對象,稱為物件參考(reference)。實際上,aPerson並不是物件本身,而是類似於指向物件的指標。 aPerson存在於記憶體的堆疊(stack)中。
3.當我們用等號賦值時,是將右側new在堆中創建物件的位址賦予給物件參考。
這裡的內存,指的是JVM (Java Virtual Machine)虛擬出來的Java進程內存空間。記憶體的堆和堆疊概念可參考Linux從程式到進程。
棧的讀取速度比堆疊快,但棧上儲存的資料受到有效範圍的限制。在C語言中,當一次函數呼叫結束時,對應的堆疊幀(stack frame)要刪除,棧幀上儲存的參量和自動變數就消失了。 Java的堆疊也受到同樣的限制,當一次方法呼叫結束,該方法儲存在堆疊上的資料將會清空。在Java中,所有的(普通)物件都儲存在堆上。因此,new關鍵字的完整意義是,在堆上建立物件。
基本型別(primitive type)的對象,如int, double,保存在堆疊上。當我們聲明基本型別時,不需要new。一旦聲明,Java將在堆疊上直接儲存基本類型的資料。所以,基本型別的變數名表示的是資料本身,不是引用。
引用和物件的關係就像風箏和人。我們看天空時(程式裡寫的),看到的是風箏(引用),但風箏下面對應的,是人(物件):
引用和物件分離;引用指向對象
儘管引用和物件是分離的,但我們所有通往物件的存取必須經過引用這個“大門”,例如以引用.方法() 的方式存取物件的方法。在Java中,我們不能跳過引用去直接接觸物件。再例如,物件a的資料成員如果是一個普通物件b,a的資料成員保存的是指向物件b的引用(如果是基本型別變量,那麼a的資料成員保存的是基本型別變數本身了)。
在Java中,引用起到了指標的作用,但我們不能直接修改指標的值,例如像C語言一樣將指標值加1。我們只能透過引用執行對物件的操作。這樣的設計避免了許多指針可能造成的錯誤。
引用的賦值
當我們將一個引用賦值給另一個引用時,我們實際上複製的是物件的位址。兩個引用將指向同一物件。例如dummyPerson=aPerson;,將導致:
一個物件可以有多個引用(一個人可以放多個風箏)。當程式透過某個引用修改物件時,透過其他引用也可以看到該修改。我們可以用以下Test類別來測試實際效果:
複製代碼代碼如下:
public class Test
{
public static void main(String[] args)
{
Human aPerson = new Human(160);
Human dummyPerson = aPerson;
System.out.println(dummyPerson.getHeight());
aPerson.growHeight(20);
System.out.println(dummyPerson.getHeight());
}
}
我們對aPerson的修改將影響到dummyPerson。這兩個引用實際上指向同一物件。
所以,將一個引用賦值給另一個引用,並不能複製物件本身。我們必須尋求其他的機制來複製物件。
垃圾回收
隨著方法呼叫的結束,引用和基本型別變數會被清空。由於物件存活於堆,所以物件所佔據的記憶體不會隨著方法呼叫的結束而清空。進程空間可能很快就會被不斷創建的物件佔滿。 Java內建有垃圾回收(garbage collection)機制,用於清空不再使用的對象,以回收記憶體空間。
垃圾回收的基本原則是,當存在引用指向某個物件時,那麼該物件不會被回收; 當沒有任何引用指向某個物件時,該物件被清空。它所佔據的空間被回收。
上圖假設了某個時刻JVM中的記憶體狀態。 Human Object有三個引用: 來自堆疊的aPerson和dummyPerson,以及另一個物件的資料成員president。而Club Object沒有引用。如果這時候垃圾回收啟動,那麼Club Object將會被清空,而Human Object來自Club Object的引用(president)也隨之被刪除。
垃圾回收是Java中重要的機制,它直接影響了Java的運作效率。我將在以後深入其細節。
參數傳遞
當我們分離了引用和物件的概念後,Java方法的參數傳遞機制實際上非常清晰: Java的參數傳遞為值傳遞。也就是說,當我們傳遞一個參數時,方法將會得到該參數的一個拷貝。
實際上,我們傳遞的參數,一個是基本類型的變量,另一個是物件的參考。
基本類型變數的值傳遞,意味著變數本身被複製,並傳遞給Java方法。 Java方法對變數的修改不會影響到原變數。
引用的值傳遞,表示物件的位址被複製,並傳遞給Java方法。 Java方法根據該引用的存取將會影響物件。
這裡有另一個值得一提的情況: 我們在方法內部使用new創建對象,並將該對象的引用返回。如果該回傳被一個引用接收,由於物件的引用不為0,物件依然存在,不會被垃圾回收。
總結
new
引用,對象
被垃圾回收的條件
參數: 值傳遞