淺談一下JAVA對象,對象引用以及對象指派
今天有班級同學問起JAVA對象的引用是什麼。正好趁着這次機會,自己總結一下JAVA對象,對象引用以及對象指派。自己總結了所看到的網上相關方面的不少文章,整理彙總形成下面的文章。
Java對象及其引用
初學Java,總是會自覺或不自覺地把Java和C++相比較。在學習Java類與對象章節的時候,發現教科書和許多參考書把對象和對象的引用混為一談。可是,如果分不清對象與對象引用, 那實在沒法很好地了解下面的面向對象技術。把自己的一點認識寫下來,或許能讓初學Java的朋友們少走一點彎路。
為便于說明,我們先定義一個簡單的類:
class Vehicle {
int passengers;
int fuelcap;
int mpg;
}
有了這個模闆,就可以用它來建立對象:
Vehicle veh1 = new Vehicle();
通常把這條語句的動作稱之為建立一個對象,其實,它包含了四個動作。
1)右邊的“new Vehicle”,是以Vehicle類為模闆,在堆空間裡建立一個Vehicle類對象(也簡稱為Vehicle對象)。
2)末尾的()意味着,在對象建立後,立即調用Vehicle類的構造函數,對剛生成的對象進行初始化。構造函數是肯定有的。如果你沒寫,Java會給你補上一個預設的構造函數。
3)左邊的“Vehicle veh 1”建立了一個Vehicle類引用變量。所謂Vehicle類引用,就是以後可以用來指向Vehicle對象的對象引用。
4)“=”操作符使對象引用指向剛建立的那個Vehicle對象。
我們可以把這條語句拆成兩部分:
Vehicle veh1;
veh1 = new Vehicle();
效果是一樣的。這樣寫,就比較清楚了,有兩個實體:一是對象引用變量,一是對象本身。
在堆空間裡建立的實體,與在資料段以及棧空間裡建立的實體不同。盡管它們也是确确實實存在的實體,但是,我們看不見,也摸不着。不僅如此,我們仔細研究一下第二句,找找剛建立的對象叫什麼名字?有人說,它叫“Vehicle”。不對,“Vehicle”是類(對象的建立模闆)的名字。
一個Vehicle類可以據此建立出無數個對象,這些對象不可能全叫“Vehicle”。
對象連名都沒有,沒法直接通路它。我們隻能通過對象引用來間接通路對象。
為了形象地說明對象、引用及它們之間的關系,可以做一個或許不很妥當的比喻。對象好比是一隻很大的氣球,大到我們抓不住它。引用變量是一根繩, 可以用來系汽球。
如果隻執行了第一條語句,還沒執行第二條,此時建立的引用變量veh1還沒指向任何一個對象,它的值是null。引用變量可以指向某個對象,或者為null。
它是一根繩,一根還沒有系上任何一個汽球的繩。執行了第二句後,一隻新汽球做出來了,并被系在veh1這根繩上。我們抓住這根繩,就等于抓住了那隻汽球。
再來一句:
Vehicle veh2;
就又做了一根繩,還沒系上汽球。如果再加一句:
veh2 = veh1;
系上了。這裡,發生了複制行為。但是,要說明的是,對象本身并沒有被複制,被複制的隻是對象引用。結果是,veh2也指向了veh1所指向的對象。兩根繩系的是同一隻汽球。
如果用下句再建立一個對象:
veh2 = new Vehicle();
則引用變量veh2改指向第二個對象。
從以上叙述再推演下去,我們可以獲得以下結論:
(1)一個對象引用可以指向0個或1個對象(一根繩子可以不系汽球,也可以系一個汽球);
(2)一個對象可以有N個引用指向它(可以有N條繩子系住一個汽球)。
如果再來下面語句:
veh1 = veh2;
按上面的推斷,veh1也指向了第二個對象。這個沒問題。問題是第一個對象呢?沒有一條繩子系住它,它飛了。多數書裡說,它被Java的垃圾回收機制回收了。這不确切。正确地說,它已成為垃圾回收機制的處理對象。至于什麼時候真正被回收,那要看垃圾回收機制的心情了。
由此看來,下面的語句應該不合法吧?至少是沒用的吧?
new Vehicle();
不對。它是合法的,而且可用的。譬如,如果我們僅僅為了列印而生成一個對象,就不需要用引用變量來系住它。最常見的就是列印字元串:
System.out.println(“I am Java!”);
字元串對象“I am Java!”在列印後即被丢棄。有人把這種對象稱之為臨時對象。對象與引用的關系将持續到對象回收。
另一個角度分析Java對象和引用以及與其密切相關的參數傳遞
先看下面的程式:
StringBuffer s;
s = new StringBuffer("Hello World!");
第一個語句僅為引用(reference)配置設定了空間,而第二個語句則通過調用類(StringBuffer)的構造函數StringBuffer(String str)為類生成了一個執行個體(或稱為對象)。這兩個操作被完成後,對象的内容則可通過s進行通路——在Java裡都是通過引用來操縱對象的。
Java對象和引用的關系可以說是互相關聯,卻又彼此獨立。彼此獨立主要表現在:引用是可以改變的,它可以指向别的對象,譬如上面的s,你可以給它另外的對象,如:
s = new StringBuffer("Java");這樣一來,s就和它指向的第一個對象脫離關系。
從存儲空間上來說,對象和引用也是獨立的,它們存儲在不同的地方,對象一般存儲在堆中,而引用存儲在速度更快的堆棧中。
引用可以指向不同的對象,對象也可以被多個引用操縱,如:
StringBuffer s1 = s;
這條語句使得s1和s指向同一個對象。既然兩個引用指向同一個對象,那麼不管使用哪個引用操縱對象,對象的内容都發生改變,并且隻有一份,通過s1和s得到的内容自然也一樣,(String除外,因為String始終不變,String s1=”AAAA”; String s=s1,操作s,s1由于始終不變,是以為s另外開辟了空間來存儲s,)如下面的程式:
StringBuffer s;
s = new StringBuffer("Java");
StringBuffer s1 = s;
s1.append(" World");
System.out.println("s1=" + s1.toString());//列印結果為:s1=Java World
System.out.println("s=" + s.toString());//列印結果為:s=Java World
上面的程式表明,s1和s列印出來的内容是一樣的,這樣的結果看起來讓人非常疑惑,但是仔細想想,s1和s隻是兩個引用,它們隻是操縱杆而已,它們指向同一個對象,操縱的也是同一個對象,通過它們得到的是同一個對象的内容。這就像汽車的刹車和油門,它們操縱的都是車速,假如汽車開始的速度是80,然後你踩了一次油門,汽車加速了,假如車速升到了120,然後你踩一下刹車,此時車速是從120開始下降的,假如下降到60,再踩一次油門,車速則從60開始上升,而不是從第一次踩油門後的120開始。也就是說車速同時受油門和刹車影響,它們的影響是累積起來的,而不是各自獨立(除非刹車和油門不在一輛車上)。是以,在上面的程式中,不管使用s1還是s操縱對象,它們對對象的影響也是累積起來的(更多的引用同理)。
隻有了解了對象和引用的關系,才能了解參數傳遞。
一般面試題中都會考Java傳參的問題,并且它的标準答案是Java隻有一種參數傳遞方式:那就是按值傳遞,即Java中傳遞任何東西都是傳值。如果傳入方法的是基本類型的東西,你就得到此基本類型的一份拷貝。如果是傳遞引用,就得到引用的拷貝。
一般來說,對于基本類型的傳遞,我們很容易了解,而對于對象,總讓人感覺是按引用傳遞,看下面的程式:
public class ObjectRef {
//基本類型的參數傳遞
public static void testBasicType(int m) {
System.out.println("m=" + m);//m=50
m = 100;
System.out.println("m=" + m);//m=100
}
//參數為對象,不改變引用的值
public static void add(StringBuffer s) {
s.append("_add");
}
//參數為對象,改變引用的值
public static void changeRef(StringBuffer s) {
s = new StringBuffer("Java");
}
public static void main(String[] args) {
int i = 50;
testBasicType(i);
System.out.println(i);//i=50
StringBuffer sMain = new StringBuffer("init");
System.out.println("sMain=" + sMain.toString());//sMain=init
add(sMain);
System.out.println("sMain=" + sMain.toString());//sMain=init_add
changeRef(sMain);
System.out.println("sMain=" + sMain.toString());//sMain=init_add
}
}
以上程式的允許結果顯示出,testBasicType方法的參數是基本類型,盡管參數m的值發生改變,但并不影響i。
add方法的參數是一個對象,當把sMain傳給參數s時,s得到的是sMain的拷貝,是以s和sMain指向同一個對象,是以,使用s操作影響的其實就是sMain指向的對象,故調用add方法後,sMain指向的對象的内容發生了改變。
在changeRef方法中,參數也是對象,當把sMain傳給參數s時,s得到的是sMain的拷貝,但與add方法不同的是,在方法體内改變了s指向的對象(也就是s指向了别的對象,牽着氣球的繩子換氣球了),給s重新指派後,s與sMain已經毫無關聯,它和sMain指向了不同的對象,是以不管對s做什麼操作,都不會影響sMain指向的對象,故調用changeRef方法前後sMain指向的對象内容并未發生改變。
對于add方法的調用結果,可能很多人會有這種感覺:這不明明是按引用傳遞嗎?對于這種問題,還是套用Bruce Eckel的話:這依賴于你如何看待引用,最終你會明白,這個争論并沒那麼重要。真正重要的是,你要了解,傳引用使得(調用者的)對象的修改變得不可預期。
public class Test{
public int i,j;
public void test_m(Test a){
Test b = new Test();
b.i = 1;
b.j = 2;
a = b;
}
public void test_m1(Test a){
a.i = 1;
a.j = 2;
}
public static void main(String argv[]){
Test t = new Test();
t.i = 5;
t.j = 6;
System.out.println( "t.i = "+ t.i + " t.j= " + t.j); //5,6
t.test_m(t);
System.out.println( "t.i = "+ t.i + " t.j= " + t.j); //5,6,a和t都指向了一個對象,而在test_m中s又指向了另一個對象,是以對象t不變!!!
t.test_m1(t);
System.out.println( "t.i = "+ t.i + " t.j= " + t.j); //1,2
}
}
答案隻有一個:Java裡都是按值傳遞參數。而實際上,我們要明白,當參數是對象時,傳引用會發生什麼狀況(就像上面的add方法)。
=========================================================================
樓主,這樣來記這個問題
如下表達式:
A a1 = new A();
它代表A是類,a1是引用,a1不是對象,new A()才是對象,a1引用指向new A()這個對象。
在JAVA裡,“=”不能被看成是一個指派語句,它不是在把一個對象賦給另外一個對象,它的執行過程實質上是将右邊對象的位址傳給了左邊的引用,使得左邊的引用指向了右邊的對象。JAVA表面上看起來沒有指針,但它的引用其實質就是一個指針,引用裡面存放的并不是對象,而是該對象的位址,使得該引用指向了對象。在JAVA裡,“=”語句不應該被翻譯成指派語句,因為它所執行的确實不是一個指派的過程,而是一個傳位址的過程,被譯成指派語句會造成很多誤解,譯得不準确。
再如:
A a2;
它代表A是類,a2是引用,a2不是對象,a2所指向的對象為空null;
再如:
a2 = a1;
它代表,a2是引用,a1也是引用,a1所指向的對象的位址傳給了a2(傳址),使得a2和a1指向了同一對象。
綜上所述,可以簡單的記為,在初始化時,“=”語句左邊的是引用,右邊new出來的是對象。
在後面的左右都是引用的“=”語句時,左右的引用同時指向了右邊引用所指向的對象。
再所謂執行個體,其實就是對象的同義詞。
如果需要指派,就需要類實作Cloneable接口,實作clone()方法。
|
指派的時候:
|
如果類中的變量不是主類型,而是對象,也需要調用該對象的clone()方法
下面是一個完整的例子:
|
Java強引用、 軟引用、 弱引用、虛引用
既然讨論到了java的引用問題,自然就會想到java的引用分為幾種情況,下面詳細說一下Java的四種引用。
1、對象的強、軟、弱和虛引用
在JDK 1.2以前的版本中,若一個對象不被任何變量引用,那麼程式就無法再使用這個對象。也就是說,隻有對象處于可觸及(reachable)狀态,程式才能使用它。從JDK 1.2版本開始,把對象的引用分為4種級别,進而使程式能更加靈活地控制對象的生命周期。這4種級别由高到低依次為:強引用、軟引用、弱引用和虛引用。

圖1
圖1為對象應用類層次
1)強引用(StrongReference)
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛拟機甯願抛出OutOfMemoryError錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足的問題。
2)軟引用(SoftReference)
如果一個對象隻具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些對象的記憶體。隻要垃圾回收器沒有回收它,該對象就可以被程式使用。軟引用可用來實作記憶體敏感的高速緩存(下文給出示例)。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛拟機就會把這個軟引用加入到與之關聯的引用隊列中。
3) 弱引用(WeakReference)
弱引用與軟引用的差別在于:弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。不過,由于垃圾回收器是一個優先級很低的線程,是以不一定會很快發現那些隻具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛拟機就會把這個弱引用加入到與之關聯的引用隊列中。
4)虛引用(PhantomReference)
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個差別在于:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之 關聯的引用隊列中。
程式可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否将要被垃圾回收。如果程式發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前采取必要的行動。
2、對象可及性的判斷
在很多時候,一個對象并不是從根集直接引用的,而是一個對象被其他對象引用,甚至同時被幾個對象所引用,進而構成一個以根集為頂的樹形結構。如圖2所示
在這個樹形的引用鍊中,箭頭的方向代表了引用的方向,所指向的對象是被引用對象。由圖可以看出,從根集到一個對象可以由很多條路徑。比如到達對象5的路徑就有①-⑤,③-⑦兩條路徑。由此帶來了一個問題,那就是某個對象的可達性如何判斷:
單條引用路徑可達性判斷:在這條路徑中,最弱的一個引用決定對象的可達性。
多條引用路徑可達性判斷:幾條路徑中,最強的一條的引用決定對象的可達性。
比如,我們假設圖2中引用①和③為強引用,⑤為軟引用,⑦為弱引用,對于對象5按照這兩個判斷原則,路徑①-⑤取最弱的引用⑤,是以該路徑對對象5的引用為軟引用。同樣,③-⑦為弱引用。在這兩條路徑之間取最強的引用,于是對象5是一個軟可達對象。
3、使用軟引用建構敏感資料的緩存
3.1 為什麼需要使用軟引用
首先,我們看一個雇員資訊查詢系統的執行個體。我們将使用一個Java語言實作的雇員資訊查詢系統查詢存儲在磁盤檔案或者資料庫中的雇員人事檔案資訊。作為一個使用者,我們完全有可能需要回頭去檢視幾分鐘甚至幾秒鐘前檢視過的雇員檔案資訊(同樣,我們在浏覽WEB頁面的時候也經常會使用“後退”按鈕)。這時我們通常會有兩種程式實作方式:一種是把過去檢視過的雇員資訊儲存在記憶體中,每一個存儲了雇員檔案資訊的Java對象的生命周期貫穿整個應用程式始終;另一種是當使用者開始檢視其他雇員的檔案資訊的時候,把存儲了目前所檢視的雇員檔案資訊的Java對象結束引用,使得垃圾收集線程可以回收其所占用的記憶體空間,當使用者再次需要浏覽該雇員的檔案資訊的時候,重新建構該雇員的資訊。很顯然,第一種實作方法将造成大量的記憶體浪費,而第二種實作的缺陷在于即使垃圾收集線程還沒有進行垃圾收集,包含雇員檔案資訊的對象仍然完好地儲存在記憶體中,應用程式也要重新建構一個對象。我們知道,通路磁盤檔案、通路網絡資源、查詢資料庫等操作都是影響應用程式執行性能的重要因素,如果能重新擷取那些尚未被回收的Java對象的引用,必将減少不必要的通路,大大提高程式的運作速度。
3.2 如果使用軟引用
SoftReference的特點是它的一個執行個體儲存對一個Java對象的軟引用,該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收。也就是說,一旦SoftReference儲存了對一個Java對象的軟引用後,在垃圾線程對這個Java對象回收前,SoftReference類所提供的get()方法傳回Java對象的強引用。另外,一旦垃圾線程回收該Java對象之後,get()方法将傳回null。看下面代碼:
MyObject aRef = new MyObject();
SoftReference aSoftRef = new SoftReference( aRef );
此時,對于這個MyObject對象,有兩個引用路徑,一個是來自SoftReference對象的軟引用,一個來自變量aRef的強引用,是以這個MyObject對象是強可及對象。
随即,我們可以結束aRef對這個MyObject執行個體的強引用:
aRef = null ;
此後,這個MyObject對象成為了軟可達對象。如果垃圾收集線程進行記憶體垃圾收集,并不會因為有一個SoftReference對該對象的引用而始終保留該對象。Java虛拟機的垃圾收集線程對軟可達對象和其他一般Java對象進行了差別對待:軟可及對象的清理是由垃圾收集線程根據其特定算法按照記憶體需求決定的。也就是說,垃圾收集線程會在虛拟機抛出OutOfMemoryError之前回收軟可及對象,而且虛拟機會盡可能優先回收長時間閑置不用的軟可達對象,對那些剛剛建構的或剛剛使用過的軟可達對象會被虛拟機盡可能保留。在回收這些對象之前,我們可以通過:
MyObject anotherRef =(MyObject) aSoftRef .get()
重新獲得對該執行個體的強引用。而回收之後,調用get()方法就隻能得到null了。
3.3 使用ReferenceQueue清除失去了軟引用對象的SoftReference
作為一個Java對象,SoftReference對象除了具有儲存軟引用的特殊性之外,也具有Java對象的一般性。是以,當軟可及對象被回收之後,雖然這個SoftReference對象的get()方法傳回null,但這個SoftReference對象已經不再具有存在的價值,需要一個适當的清除機制,避免大量SoftReference對象帶來的記憶體洩漏。在java.lang.ref包裡還提供了ReferenceQueue。如果在建立SoftReference對象的時候,使用了一個ReferenceQueue對象作為參數提供給SoftReference的構造方法,如:
ReferenceQueue queue = new ReferenceQueue();
SoftReference ref = new SoftReference( aMyObject , queue );
那麼當這個SoftReference所軟引用的aMyOhject被垃圾收集器回收的同時,ref所強引用的SoftReference對象被列入ReferenceQueue。也就是說,ReferenceQueue中儲存的對象是Reference對象,而且是已經失去了它所軟引用的對象的Reference對象。另外從ReferenceQueue這個名字也可以看出,它是一個隊列,當我們調用它的poll()方法的時候,如果這個隊列中不是空隊列,那麼将傳回隊列前面的那個Reference對象。
在任何時候,我們都可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的非強可達對象被回收。如果隊列為空,将傳回一個null,否則該方法傳回隊列中前面的一個Reference對象。利用這個方法,我們可以檢查哪個SoftReference所軟引用的對象已經被回收。于是我們可以把這些失去所軟引用的對象的SoftReference對象清除掉。常用的方式為:
SoftReference ref = null ;
while ((ref = (SoftReference)q .poll()) != null ) {
// 清除 ref
}
了解了ReferenceQueue的工作機制之後,我們就可以開始構造一個Java對象的高速緩存器了。
3.4通過軟可及對象重獲方法實作Java對象的高速緩存
利用Java2平台垃圾收集機制的特性以及前述的垃圾對象重獲方法,我們通過一個雇員資訊查詢系統的小例子來說明如何建構一種高速緩存器來避免重複建構同一個對象帶來的性能損失。我們将一個雇員的檔案資訊定義為一個Employee類:
public class Employee {
private String id ; // 雇員的辨別号碼
private String name ; // 雇員姓名
private String department ; // 該雇員所在部門
private String Phone ; // 該雇員聯系電話
private int salary ; // 該雇員薪資
private String origin ; // 該雇員資訊的來源
// 構造方法
public Employee(String id) {
this . id = id;
getDataFromlnfoCenter();
}
// 到資料庫中取得雇員資訊
private void getDataFromlnfoCenter() {
// 和資料庫建立連接配接井查詢該雇員的資訊,将查詢結果指派
// 給 name, department, plone, salary等變量
// 同時将 origin指派為 "From DataBase"
}
……
}
這個Employee類的構造方法中我們可以預見,如果每次需要查詢一個雇員的資訊。哪怕是幾秒中之前剛剛查詢過的,都要重新建構一個執行個體,這是需要消耗很多時間的。下面是一個對Employee對象進行緩存的緩存器的定義:
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Hashtable;
public class EmployeeCache {
static private EmployeeCache cache ; // 一個 Cache執行個體
private Hashtable<String, EmployeeRef> employeeRefs ; // 用于 Chche内容的存儲
private ReferenceQueue<Employee> q ; // 垃圾 Reference的隊列
// 繼承 SoftReference,使得每一個執行個體都具有可識别的辨別,
// 并且該辨別與其在 HashMap内的 key相同。
private class EmployeeRef extends SoftReference<Employee> {
private String _key = "" ;
public EmployeeRef(Employee em, ReferenceQueue<Employee> q) {
super (em, q);
_key = em.getID();
}
}
// 建構一個緩存器執行個體
private EmployeeCache() {
employeeRefs = new Hashtable<String,EmployeeRef>();
q = new ReferenceQueue<Employee>();
}
// 取得緩存器執行個體
public static synchronized EmployeeCache getInstance() {
if (cache == null){
cache = new EmployeeCache();
}
return cache ;
}
// 以軟引用的方式對一個 Employee對象的執行個體進行引用并儲存該引用
private void cacheEmployee(Employee em) {
cleanCache(); // 清除垃圾引用
EmployeeRef ref = new EmployeeRef(em, q );
employeeRefs .put(em.getID(), ref);
}
// 依據所指定的 ID号,重新擷取相應 Employee對象的執行個體
public Employee getEmployee(String ID) {
Employee em = null ;
// 緩存中是否有該 Employee執行個體的軟引用,如果有,從軟引用中取得。
if ( employeeRefs .containsKey(ID)) {
EmployeeRef ref = (EmployeeRef) employeeRefs .get(ID);
em = (Employee) ref.get();
}
// 如果沒有軟引用,或者從軟引用中得到的執行個體是 null,重新建構一個執行個體,
// 并儲存對這個建立執行個體的軟引用
if (em == null ) {
em = new Employee(ID);
System. out .println( "Retrieve From EmployeeInfoCenter. ID=" + ID);
this .cacheEmployee(em);
}
return em;
}
// 清除那些所軟引用的 Employee對象已經被回收的 EmployeeRef對象
private void cleanCache() {
EmployeeRef ref = null ;
while ((ref = (EmployeeRef) q .poll()) != null ) {
employeeRefs .remove(ref. _key );
}
}
}
結語:
上述是從自己見過的感覺對了解Java對象,對象引用以及對象指派,目前最優價值的文章中挑選而來。希望對大家有用。
主要參考資料:
http://blog.sina.com.cn/s/blog_4cd5d2bb0100ve9r.html
http://bxkqzjt521.blog.163.com/blog/static/202818420119102362458/
http://www.2cto.com/kf/201207/139522.html