轉載自:http://github.thinkingbar.com/thinking_in_java_chapter05/
C++提出了構造函數和析構函數。爲了提高效率,這兩項工作均由程序員完成。而Java僅提供了構造函數供程序員使用,對於清理工作,Java自帶垃圾回收器。雖然安全性有了保證,但是也犧牲了一定的效率。
1. this關鍵字
假如A類有一個fun(int i)函數,現在A類創建了2個對象,2個引用指向了這兩個新對象,2個引用爲a和b。那麼a和b調用fun(int i)的時候,fun(int i)怎麼區分哪個是a哪個是b呢?其實這個工作是通過this實現的。編譯器在編譯的時候,會把a.fun(1)
和b.fun(2)
轉化成A.fun(a,
1)
和A.fun(b, 2)
的。當然,這是編譯器在幕後做的,我們使用的時候不能寫成這種形式。
2. 構造函數中調用構造函數
在構造函數中調用其他構造函數時,有兩個規定:
- this調用其他構造函數必須是當前構造函數的第一個語句。然後就很清楚了,前面不能有任何其他語句(所以不能有2個及以上個this調用。因爲this調用必須是第一個語句,所以第二個this調用前面也有一個語句(不管是什麼語句,只要有,就出錯)。
- 只能在構造函數中才能通過this調用構造函數,在非構造函數中不能通過this調用構造函數。
3. 對static的一點討論
- 對於static來說,其實有一些爭議。因爲我們知道static是類的屬性,與對象無關。但是Java標榜自己是完全面向對象的程序語言。類與類之間的溝通完全是通過對象的消息進行通信的。但是static卻和對象沒有關係,所以違背了Java的面向對象的說法。但是,我們知道事情都有兩面性,在適當的時候能運用static還是非常有必要的,只是如果你程序中大量出現static的時候,就應該重新考慮一下設計的是否合理了。
4. Java垃圾回收
首先大概瞭解一下Java垃圾回收機制的幾種工作方式:
時刻牢記:使用垃圾回收器的唯一原因是爲了回收程序不再使用的內存。所以對於與垃圾回收有關的任何行爲來說(尤其是finalize()方法),它們也必須同內存及其回收有關。意思就是說,如果你的程序中有一個地方跟內存無關,那麼垃圾回收器就不會管。比如Java中調用的C/C++程序,而C/C++程序申請/釋放內存的話與Java無關,那麼Java垃圾回收器就不會對這個內存起作用。而finalize()就是爲解決這個問題提出的,先看以下三點:
- 對象可能不被垃圾回收
- 垃圾回收並不等於”析構造”
- 垃圾回收只與內存有關
Java引入finalize()的原因是因爲在分配內存時可能採用了類型C語言中的語法,而非Java中的通常做法。這種情況多發生在使用”本地方法”的情況下,本地方法是一種在Java中調用非Java代碼的方式。本地方法目前只支持C/C++,但是在C/C++中又可以調用其他語言,所以從本質上來說,Java程序中可能出現所有編程語言。當然,每種編程語言有自己不同的內存分配策略,因爲這與Java無關,所以Java的垃圾回收機制就不會作用於這些內存,當然需要程序員自己處理了。比如C代碼中通過malloc()分配存儲空間,除非調用了free()來釋放這些存儲空間,否則存儲空間是不會釋放的,這樣就會造成內存泄露。當然,free()是C中的函數,必須在finalize()中用本地方法(這裏是C方法)來調用它。
關於finalize()的知識,本書並沒有過多涉及,如果想參考,可以看下《深入理解Java虛擬機》這本書,上面寫的很詳細。
5. 初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package Chapter05; class InitTest { public static int number = 2014; public int i; public int j = 3; InitTest(int i) { this.i = i; j = 10; } public void print() { System.out.println(number); System.out.println(i); System.out.println(j); } } public class InitOrder { public static void main(String[] args) { InitTest initTest = new InitTest(20); initTest.print(); } } |
- 當首次創建類型爲InitTest的對象時(構造函數其實是靜態方法),或者InitTest類的靜態方法(main)/靜態屬性首次被訪問的時候,Java解釋器會根據CLASSPATH變量查找類路徑,定位InitTest.class文件
- 然後使用classLoader載入InitTest.class(創建一個Class實例),有關靜態初始化的所有動作都會執行,因此靜態初始化只在class對象首次加載的時候進行一次(如果沒有針對對象的new操作,初始化就完成了。接下來是創建對象纔會觸發的)
- 如果用new InitTest()創建對象,會在堆上爲InitTest對象分配足夠的存儲空間
- 這塊存儲空間首先會被清零,自動將InitTest對象中的所有基本類型數據設置成默認值,引用初始化爲null
- 執行所有出現於數據定義處的初始化動作
- 執行構造函數
所以上面的例子工作流程是這樣的:
- 當在InitOrder中調用了public static void main(String[] args)就觸發了InitTest的靜態main方法,然後Java解釋器通過CLASSPATH查找類路徑,定位InitTest.class
- 載入InitTest.class後,得到一個Class的實例,將static屬性的number初始化爲2014
- 因爲有new操作,會在堆上爲InitTest分配存儲空間
- 內存首先被清空,i和j都會被置0(如果有引用的話,引用會被置null)
- j在定義處有初始化操作,所以j被重置爲3
- 執行構造函數,i被重置爲20,j被重置爲10
有兩點需要注意的是:
1.對於static域,可以在定義的地方初始化,也可以在構造函數初始化。所以不能認爲構造函數只是初始化非static域。而且對於static域,可以將static變量統一賦值。對於普通變量也可以使用類似的方法,只要在”{“前面去掉static就可以了。這種語法對於支持“匿名內部類”的初始化是必須的,但是它也使得你可以保證無論調用了哪個顯式構造函數,某些操作都會發生。
1 2 3 4 5 6 | static int i, j; static { i = 1; j = 2; } |
2.對於局部變量,你不初始化,編譯時候肯定出錯。因爲局部變量說明程序員在這裏用到才設置的,所以要提醒你。比如在C++中定義這樣的
1 2 3 4 5 6 | { int i; i++; cout<<i<<endl; } |
你運行的時候都不會出錯,但是這個i到底是多少就不知道了。所以排查起來非常晦澀;而在Java中,你編譯的時候就會出錯(對於Eclipse來說,你寫完這句話就會出現錯誤提示,提示你本地變量沒有初始化),所以Java這點:編譯器可以爲它賦值,但是未初始化的局部變量更可能是程序員的疏忽,採用默認值反而會掩蓋錯誤。因此強制程序員提供一個初始值,往往能夠找出程序的缺陷。
6. 創建數組的一個坑
- 如果是基本類型,你創建之後就可以使用了。默認初始化爲0
- 如果是對象,那麼你創建的只是一個引用數組,數組中的元素都是引用,但是沒有指向具體的對象。所以你使用之前必須讓它們指向對象
7. 參數維護
Java初始化數組有幾個點和C++不一樣,看head first Java裏沒講到這兩個點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Integer[] a = new Integer[20]; for(int i = 0; i \< 20; ++i) { a[i] = new Integer(i); } Integer[] b = { new Integer(1), new Integer(2), 3, //autoboxing }; Integer[] c = new Integer[]{ new Integer(1), new Integer(2), 3, //autoboxing }; |
- 發現3後面都有一個逗號,這一特性使得維護長列表變得更容易一些
- 比如:fun1(String[] strings)調用
fun1(new String[]{ "one", "two", "three"});
8. 可變形參
Java在SE 5提出了可變形參的概念,然後就再也不用顯式地編寫數組語法了,當你指定參數時,編譯器實際上會爲你去填充數組。但是我沒覺得這玩意有什麼大的用處啊?string []和string …不都是實現相同的功能嗎?不都是string個數不確定的意思嗎?然後到Java論壇得到了滿意的答覆:
數組的功能是可變參數功能的子集,凡是用數組做形參的地方都可以用可變參數替換。可變參數的功能比數組更爲強大,除了能夠接收數組類型的實參外,還能夠接收個數不限的單個實參。寫成“String… args”這樣子,實際上是提醒編譯器將接收到的若干單個實參整理成數組傳給args,args歸根結底接收到的還是數組。當然,你若直接給args傳一個數組也沒有錯,這樣反而省的編譯器去整理了。
所以說,對於可能出現多個甚至1個參數的情況下,我們只能使用可變形參,因爲[]無法接受1個String引用。而且需要注意的一點是:可變形參必須是參數列表的最後一個,要不然會出現傻傻分不清楚的問題,大家懂的
9. 對於super和this的理解
super和this本來是沒有聯繫的,因爲super是調用父類的函數,而this是調用本類的函數。但它們都算是一種代碼複用吧?所以就放在一起說一下:
- super:是因爲子類需要在父類基礎上再增加一些東西,比如起牀函數,基類完成穿衣服、洗臉刷牙2個步驟,而由於子類是個近視,所以需要完成的有穿衣服、洗臉刷牙、戴眼鏡三個,而前兩個基類已經完成,你就可以調用super(),然後加入戴眼鏡邏輯即可。
- super:和繼承中的構造函數也有關係,當基類自定義了有參構造函數後,子類必須顯示定義構造函數,並使用super來初始化基類。
- this:this是用在本類中的,它的作用就是讓一個構造函數來調用其他重載的構造函數,比如穿衣服初始化,一個構造函數是穿一個背心就搞定,另一個構造函數是穿一個厚外套,那麼,在天氣熱的時候調用第一個構造函數;在天氣冷的時候,我們在厚外套這個構造函數中先調用穿背心,然後再穿厚外套,就不用重複造“穿背心”這個輪子了。