Java 初始化與清理

轉載自: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. 構造函數中調用構造函數

在構造函數中調用其他構造函數時,有兩個規定:

  1. this調用其他構造函數必須是當前構造函數的第一個語句。然後就很清楚了,前面不能有任何其他語句(所以不能有2個及以上個this調用。因爲this調用必須是第一個語句,所以第二個this調用前面也有一個語句(不管是什麼語句,只要有,就出錯)。
  2. 只能在構造函數中才能通過this調用構造函數,在非構造函數中不能通過this調用構造函數。

3. 對static的一點討論

  1. 對於static來說,其實有一些爭議。因爲我們知道static是類的屬性,與對象無關。但是Java標榜自己是完全面向對象的程序語言。類與類之間的溝通完全是通過對象的消息進行通信的。但是static卻和對象沒有關係,所以違背了Java的面向對象的說法。但是,我們知道事情都有兩面性,在適當的時候能運用static還是非常有必要的,只是如果你程序中大量出現static的時候,就應該重新考慮一下設計的是否合理了。

4. Java垃圾回收

首先大概瞭解一下Java垃圾回收機制的幾種工作方式:

  1. Java GC 機制學習
  2. Java 垃圾回收機制

時刻牢記:使用垃圾回收器的唯一原因是爲了回收程序不再使用的內存。所以對於與垃圾回收有關的任何行爲來說(尤其是finalize()方法),它們也必須同內存及其回收有關。意思就是說,如果你的程序中有一個地方跟內存無關,那麼垃圾回收器就不會管。比如Java中調用的C/C++程序,而C/C++程序申請/釋放內存的話與Java無關,那麼Java垃圾回收器就不會對這個內存起作用。而finalize()就是爲解決這個問題提出的,先看以下三點:

  1. 對象可能不被垃圾回收
  2. 垃圾回收並不等於”析構造”
  3. 垃圾回收只與內存有關

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();
	}
}
  1. 當首次創建類型爲InitTest的對象時(構造函數其實是靜態方法),或者InitTest類的靜態方法(main)/靜態屬性首次被訪問的時候,Java解釋器會根據CLASSPATH變量查找類路徑,定位InitTest.class文件
  2. 然後使用classLoader載入InitTest.class(創建一個Class實例),有關靜態初始化的所有動作都會執行,因此靜態初始化只在class對象首次加載的時候進行一次(如果沒有針對對象的new操作,初始化就完成了。接下來是創建對象纔會觸發的)
  3. 如果用new InitTest()創建對象,會在堆上爲InitTest對象分配足夠的存儲空間
  4. 這塊存儲空間首先會被清零,自動將InitTest對象中的所有基本類型數據設置成默認值,引用初始化爲null
  5. 執行所有出現於數據定義處的初始化動作
  6. 執行構造函數

所以上面的例子工作流程是這樣的:

  1. 當在InitOrder中調用了public static void main(String[] args)就觸發了InitTest的靜態main方法,然後Java解釋器通過CLASSPATH查找類路徑,定位InitTest.class
  2. 載入InitTest.class後,得到一個Class的實例,將static屬性的number初始化爲2014
  3. 因爲有new操作,會在堆上爲InitTest分配存儲空間
  4. 內存首先被清空,i和j都會被置0(如果有引用的話,引用會被置null)
  5. j在定義處有初始化操作,所以j被重置爲3
  6. 執行構造函數,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
	};
  1. 發現3後面都有一個逗號,這一特性使得維護長列表變得更容易一些
  2. 比如: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是用在本類中的,它的作用就是讓一個構造函數來調用其他重載的構造函數,比如穿衣服初始化,一個構造函數是穿一個背心就搞定,另一個構造函數是穿一個厚外套,那麼,在天氣熱的時候調用第一個構造函數;在天氣冷的時候,我們在厚外套這個構造函數中先調用穿背心,然後再穿厚外套,就不用重複造“穿背心”這個輪子了。

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