Java開發筆記(五十五)關鍵字static的用法

pinlantu發表於2019-01-31

前面介紹巢狀類的時候講到了關鍵字static,用static修飾類,該類就變成了巢狀類。從巢狀類的用法可知,其它地方訪問巢狀類之時,無需動態建立外層類的例項,直接建立巢狀類的例項就行。
其實static不光修飾類,還能用來修飾方法、修飾屬性等等,例如大家學習Java一開始就遇到的main方法,便為static所修飾。當一個成員方法被static修飾之後,該方法就成為靜態方法;當一個成員屬性被static修飾之後,該屬性就成為靜態屬性。靜態方法和靜態屬性,它倆同巢狀類一樣不依賴於所在類的例項。外部若要訪問某個類的靜態方法,只需通過“類名.靜態方法名”即可;同理,通過“類名.靜態屬性名”就能訪問該類的靜態屬性。由於靜態方法和靜態屬性擁有獨立呼叫的特性,因此它們常常出現在一些通用的工具場景,例如系統的數學函式庫Math,便提供了大量的靜態方法和靜態屬性。其中常見的靜態方法包括四捨五入函式Math.round、取絕對值函式math.abs、求平方根函式Math.sqrt等,常見的靜態屬性則有圓周率近似值Math.PI等。
那麼開發者自己定義一個新類,如何得知哪些屬性需要宣告為靜態屬性,哪些方法需要宣告為靜態方法呢?在多數情況下,靜態屬性的取值一般要求是固定不變的,而靜態方法只允許對輸入引數進行加工,不允許操作其它的成員變數(靜態屬性除外)。以樹木類為例,凡是會動態變化著的性狀與事情,顯然不適合宣告為靜態成員;只有與生長過程無關的概念,才適合宣告為靜態成員。譬如樹木可分為喬木與灌木兩大類,可想而知喬木與灌木的型別取值,與每棵樹木的生長情況沒有關聯,這兩種樹木型別就適合作為靜態屬性。根據樹木的型別,推斷該樹木的型別名稱是“喬木”還是“灌木”,這個型別名稱的判斷方法就適合作為靜態方法。如此一來,Tree類便可新增下面的靜態成員宣告程式碼:

	// static的字面意思是“靜態的”,意味著無需動態建立即可直接使用。
	// 利用static修飾成員屬性,外部即可通過“類名.屬性名”直接訪問靜態屬性。
	public static int TYPE_ARBOR = 1;
	public static int TYPE_BUSH = 2;

	// 利用static修飾成員方法,外部即可通過“類名.方法名”直接訪問靜態方法。
	public static String getTypeName(int type) {
		String type_name = "";
		if (type == TYPE_ARBOR) {
			type_name = "喬木";
		} else if (type == TYPE_BUSH) {
			type_name = "灌木";
		}
		return type_name;
	}

外部訪問樹木類的靜態成員,只要按照“類名.靜態成員名”的格式就好,具體的呼叫程式碼如下所示:

	// 演示靜態成員的呼叫方式
	private static void testStaticMember() {
		// 使用靜態屬性無需建立該類的例項,只要通過“類名.靜態屬性名”即可訪問靜態屬性
		System.out.println("型別TYPE_ARBOR的取值為"+TreeStatic.TYPE_ARBOR);
		System.out.println("型別TYPE_BUSH的取值為"+TreeStatic.TYPE_BUSH);
		// 使用靜態方法無需建立該類的例項,只要通過“類名.靜態方法名”即可訪問靜態方法
		String arbor_name = TreeStatic.getTypeName(TreeStatic.TYPE_ARBOR);
		System.out.println("型別TYPE_ARBOR對應的名稱是"+arbor_name);
		String bush_name = TreeStatic.getTypeName(TreeStatic.TYPE_BUSH);
		System.out.println("型別TYPE_BUSH對應的名稱是"+bush_name);
	}

神通廣大的static不僅可以修飾類、屬性、方法,它居然還能修飾一段程式碼塊!被static修飾的程式碼段樣例如下:

	static {
		// 這裡是被static修飾的程式碼段內容
	}

 

以上為static所包裹的程式碼段,又被稱作“靜態程式碼塊”,其作用是在系統載入該類之時立即執行這部分程式碼。因為此處的程式碼被static包括,所以靜態程式碼塊內部只能操作同類的靜態屬性和靜態方法,而不能操作普通的成員屬性和成員方法。可是這裡有個問題,早先提到構造方法才是建立例項之時的初始操作,那麼靜態程式碼塊與構造方法比起來,它們的執行順序孰先孰後?倘若從Java的執行機制來解答該問題,不但費口舌而且傷腦筋,都說實踐出真知,接下來不如做個實驗,看看它們究竟是怎樣的先來後到。
首先在樹木類中宣告一個靜態的整型變數leaf_count,之所以新增static修飾符,是因為要給靜態程式碼塊使用;接著在靜態程式碼塊內部對該變數做自增操作,並將變數值列印到日誌;同時在樹木類的構造方法裡面也進行leaf_count的自增運算,以及往控制檯輸出它的變數值。修改後的相關程式碼片段示例如下:

	// 葉子數量,用來演示構造方法與初始靜態程式碼塊的執行順序
	public static int leaf_count = 0;
	
	// static還能用來包裹某個程式碼塊,一旦當前類載入進記憶體,靜態程式碼塊就立即執行
	static {
		leaf_count++;
		System.out.println("這裡是初始的靜態程式碼塊,此時葉子數量為"+leaf_count);
	}

	public TreeStatic(String tree_name) {
		this.tree_name = tree_name;
		leaf_count++;
		System.out.println("這裡是構造方法,此時葉子數量為"+leaf_count);
	}

最後回到外部建立該樹木類的新例項,對應程式碼如下所示:

	// 演示靜態程式碼塊與構造方法的執行順序
	private static void testStaticBlock() {
		System.out.println("開始建立樹木類的例項");
		TreeStatic tree = new TreeStatic("月桂");
		System.out.println("結束建立樹木類的例項");
	}

 

執行以上的演示程式碼,觀察到下列的日誌資訊:

這裡是初始的靜態程式碼塊,此時葉子數量為1
開始建立樹木類的例項
這裡是構造方法,此時葉子數量為2
結束建立樹木類的例項

 

從日誌結果可見,靜態程式碼塊的內部程式碼早早就得到執行了,而構造方法的內部程式碼要等到外部呼叫new的時候才會執行,這證明了靜態程式碼塊的執行時機確實先於該類的構造方法。

靜態修飾符一邊給開發者帶來了便利,一邊也帶來了不大不小的困惑。為了說明問題的迷惑性,接下來照例做個程式碼實驗。仍舊在樹木類中先宣告一個靜態的整型變數annual_ring,再補充一個成員方法grow,該方法內部對annual_ring自增的同時也列印日誌。依據上述步驟給樹木類新增瞭如下程式碼:

	// 樹木年輪,用來演示靜態屬性的永續性
	public static int annual_ring = 0;
	
	// 注意每次讀取靜態屬性,得到的都是該屬性最近一次的數值
	public void grow() {
		annual_ring++;
		System.out.println(tree_name+"的樹齡為"+annual_ring);
	}

 

然後其它地方先後建立這個樹木類的兩個例項,就像下面程式碼示範的那樣:

	// 演示靜態屬性的永續性
	private static void testStaticProperty() {
		TreeStatic bigTree = new TreeStatic("大樹");
		bigTree.grow();
		TreeStatic littleTree = new TreeStatic("小樹");
		littleTree.grow();
	}

 

繼續執行上面的測試程式碼,發現列印的日誌如下:

這裡是構造方法,此時葉子數量為3
大樹的樹齡為1
這裡是構造方法,此時葉子數量為4
小樹的樹齡為2

 

雖然bigTree和littleTree是新建立的例項,但是從日誌結果看它們的annual_ring數值竟然是遞增的,這可真是咄咄怪事,兩個例項分明都是通過new出來的呀!產生怪異現象的罪魁禍首,原來就是static這個始作俑者,凡是被static修飾的靜態變數,它在記憶體中佔據了一塊固定的區域,不管所在類被建立了多少個例項,每個例項引用的靜態變數依然是最初分配的那個。於是後面建立的樹木例項littleTree,其內部的annual_ring與之前例項bigTree的annual_ring保持一致,無怪乎前後兩例項的annual_ring數值是依序遞增的了。
由此可見,靜態屬性總是儲存最後一次的數值,倘若它的取值每次都發生變化,即使建立新例項也得不到靜態屬性最初的數值。這種後果顯而易見違背了靜態變數的設計初衷,在多數時候,開發者定義一個靜態屬性,原本是想作為取值不變的常量使用,而不希望它變來變去。對於此類用於常量定義的靜態屬性,可以在static前頭再新增修飾符final,表示該屬性只允許賦值一次,從而避免了多次賦值導致取值更改的尷尬。下面是聯合修飾final和static的屬性定義程式碼例子:

	// 若想靜態屬性始終如一保持不變,就得給該屬性新增final修飾符,表示終態屬性只能被賦值一次
	public final static int FINAL_TYPE_ARBOR = 1;
	public final static int FINAL_TYPE_BUSH = 2;

  

更多Java技術文章參見《Java開發筆記(序)章節目錄

相關文章