標籤:初始化 java初始化 enum 靜態語句塊 初始化順序
第五章 初始化與清理5.6 成員初始化
Java儘力保證:所有變數在使用前都能得到恰當的初始化。對於方法的局部變數,Java以編譯錯誤的形式來保證。如下:
void f() { int i; i++; //Error:i not initialized}
會得到一條錯誤的訊息,提示i可能沒有初始化。編譯器可以給i賦初值,但是並沒有這麼做,因為沒有初始化是程式員的疏忽,為了保證程式的穩定性,能協助程式員找出程式的缺陷所在。
但如果是類的資料成員是基本類型,類的每個基本類型成員變數都會保證有一個初值。如下:
public class Test { boolean t; char c; byte b; short s; int i; long l; float f; double d; InitialValues reference;}
上面的一系列數值分別為:false, (空白), 0, 0, 0, 0, 0.0, 0.0, null。char值為0,所以顯示為空白。
5.6.1 指定初始化
有一種很直接的方法給某個變數賦初值,就是在定義類成員變數的地方直接為其賦值,C++中不允許。對初始化非基本類型的對象也可以,例如下面的A類的對象,這樣類Test的每個對象都會具有相同的初始值。如下:
class A{}public class Test { boolean bool = true; char ch = ‘x‘; int i = 99; A a = new A();}
5.7 構造器初始化
還可以用構造器來進行初始化。在運行時刻,可以調用方法或執行某些動作來確定初值,這給編程帶來了更大的靈活性。但是要記住,無法組織自動初始化的進行,也就是前面提到的編譯器自動賦值,這個工作在構造器被調用之前就發生。例如:
public class Test { int i; Test() {i = 7;} }
i的值首先被置為0,再被賦值為7。
5.7.1 初始化順序
在類的內部,變數定義的先後順序決定了初始化的順序。即使變數定義散布與方法定義之間,它們人就會在任何方法(包括構造器)被調用之前得到初始化。例如:
class Window { Window(int maker) { System.out.println("Window(" + maker + ")"); }}class House { Window w1 = new Window(1); House() { System.out.println("House"); w3 = new Window(33); } Window w2 = new WIndow(2); void f() { System.out.println("f()"); Window w3 = new Window(3); }}public class Test { public static void main(String[] args) { House h = new House(); h.f(); } }
結果為Window(1) Window(2) Window(3) House() Window(33) f()。
5.7.2 待用資料的初始化
無論建立多少個對象,待用資料都至佔用一份儲存地區。static關鍵字不能應用於局部變數。看下面的例子:
class Bowl { Bowl(int marker) { System.out.println("Bowl(" + marker + ")"); } void f1(int marker) { System.out.println("f1(" + marker + ")"); }}class Table { static Bowl bowl1 = new Bowl(1); Table() { System.out.println("Table()"); bowl2.f1(1); void f2(int marker) { System.out.println("f2(" + marker + ")"); } static Bowl bowl2 = new Bowl(2); }class Cupboard { Bowl bowl3 = new Bowl(3); static Bowl bowl4 = new Bowl(4); Cupboard() { System.out.println("Cupboard()"); bowl4.f1(2); } void f3(int marker) { System.out.println("f3(" + marker + ")"); } static Bowl bowl5 = new Bowl(5);}}public class Test { public static void main(String[] args){ System.out.println("Creating new Cupboard() in main"); new Cupboard(); Syste.out.println("Creating new Cupboard() in main"); new Cupboard(); table.f2(1); cupboard.f3(1); } static Table table = new Table(); static Cupboard cupboard = new Cupbpard();}
輸出的結果依次為:
Bowl(1) Bowl(2) Table() f1(1) Bowl(4)Bowl(5)Bowl(3) Cupboard()f1(2) Creating new Cupboard() in main Bowl(3) Cupboard() f1(2) Creating new Cupboard() in main Bowl(3)Cupboard()f1(2)f2(1)f3(1)
靜態初始化只有在必要的時候才會進行,只有在第一個類對象被建立(或第一次訪問待用資料)的時候,待用資料才會被初始化。之後,無論怎麼建立對象,都不會再次被初始化。
初始化的順序是先靜態對象(如果它們尚未被初始化),而後是”非靜態對象”。
總結一下對象的建立過程,假設有個名為Dog的類:
- 即使沒有顯式的使用static關鍵字,構造器實際上也是靜態方法。因此,當首次建立類型為Dog對象時(構造器可以看成靜態方法),或者Dog類的靜態方法、靜態域首次被訪問的時候,Java解譯器必須尋找類的路徑,以定位Dog.class。
- 然後載入Dog.class,有關靜態初始化的所有動作都會被執行,因此,靜態初始化只在Class對象首次載入的時候進行一次。
- 當用new建立Dog的對象時候,首先在堆上給Dog對象分配足夠的儲存空間。
- 清零所分配的儲存空間,這就自動的將Dog對象中所有的基礎資料型別 (Elementary Data Type)設定成了預設值(對數字來說就是0,對boolean和char類型的來說也類似),而引用則都被設定成了null。
- 執行所有出現於欄位定時的初始化動作。
- 執行構造器方法。
5.7.3 顯示的靜態初始化
Java允許多個靜態初始化動作組織成一個特殊的”靜態子句”(有時也叫做”靜態塊”)。就像下面這樣:
public class Spoon { static int i; static { i = 47; }}
和其他靜態初始化動作一樣,這段代碼只執行一次:當首次產生這個類的對象時,或首次訪問屬於這個類的靜態成員資料的時候執行。例如:
class Cup { Cup(int marker) { System.out.println("Cup(" + marker + ")"); } void f(int marker) { System.out.println("f(" + marker + ")"); }}class Cups { static Cup cup1; static Cup cup2; static { cup1 = new Cup(1); cup2 = new Cup(2); } Cups() { System.out.println("Cups()"); }}public class Test { public static void main(String[] args) { System.out.println("Inside main()"); Cups.cup1.f(99); // (1) } //static Cups cups1 = new Cups(); // (2) //static Cups cup2 = new Cups(); // (2)}
上面的結果為:Inside main() Cup(1) Cup(2) f(99)
5.7.4 非靜態執行個體初始化
Java中也有被成為執行個體初始化的類似文法,用來初始化每一個對象的非靜態變數。和靜態語句塊一樣的,只不過少了static。如果不建立類的執行個體,非靜態語句塊是不會被執行,只會觸碰static變數和語句塊。
下面用一個例子來總結下上述的順序:
class Cup {{ System.out.println("Block - Cup");}static int c;static { c = 1; System.out.println("Static Bolck - Cup");} Cup(int marker) { System.out.println("Construct - Cup(" + marker + ")"); } void f(int marker) { System.out.println("Function - f(" + marker + ")"); }}class Cups { static { cup1 = new Cup(1); cup2 = new Cup(2); } static Cup cup1; static Cup cup2; { System.out.println("Block - Cups"); } Cups() { System.out.println("Construct - Cups()"); }}public class JavaTest { public static void main(String[] args) { System.out.println("Inside main()"); Cups.cup1.f(99); // (1) }}
輸出的結果為:
Inside main()Static Bolck - CupBlock - CupConstruct - Cup(1)Block - CupConstruct - Cup(2)Function - f(99)
從上面的結果可以看出,沒有建立Cups類的對象時,不會執行非靜態語句塊,也就是被{}包括起來的語句塊。在第一次建立類對象或者使用到類的靜態變數的時候,就會將.class檔案載入進來,初始化static變數,執行static{}語句塊。
5.8 數組初始化
數組只是相同類型的、用一個標識符名稱封裝到一起的一個對象序列或基本類型資料序列。素組是通過方括弧下標操作符【】來定義和使用的。定義數組只需要在類型名後面加上一對方括弧:int[] a1;。方括弧也可以放在後面:int a1[];
兩種格式的含義是一樣的,後面一種格式符合C和C++程式員的習慣。前面一種格式能更直觀的看出,其表明的類型是”一個int型數組”。
編譯器不允許指定數組的大小,現在擁有的只是對數組的一個引用(你已經為該引用分配了足夠的儲存空間),而且也沒給數組對象本身分配任何空間。為了給數組建立相應的儲存空間,需要對數組進行初始化。數組的初始化代碼可以出現在代碼的任何地方,但也可以使用一種特殊的初始設定式,必須在建立數組的地方出現。這種特殊的初始化是由一對花括弧括起來的,儲存空間的分配(等價於使用new)將由編譯器負責。例如:
int[] a1 = {1,2,3,4,5};
那麼為什麼還要在沒有數組的時候定義一個數組的引用呢?Java中可以將數組賦值給另一個數組,int[] a2;,在Java中可以將一個數組賦值給另一個數組,所以可以這樣:a2 = a1;直接複製一個引用。下面的例子:
public class ArrayTest { public static void main(String[] args) { int[] a1 = {1,2,3,4,5}; int[] a2; a2 = a1; for(int i = 0;i < a2.length; i++) a2[i] = a2[i] + 1; for(int i = 0;i < a1.length; i++) System.out.println("a1[" + i + "]" + a[i]); }}
輸出為a1[0]=1 a1[1]=2 a1[2]=3 a1[3]=4 a1[4]=5
所有數組(無論元素始對象還是基本類型)都有一個固有成員length,可以通過它獲得數組長度,但其不能直接被更改。和C與C++類似,Java數組計數從0開始,數組越界,C和C++默默接受,但Java直接出現執行階段錯誤。
可以在編程時,通過new再數組裡面建立元素。儘管建立的是基本類型數組,new仍然可以工作(不能用new建立單個的基本類型資料)。
public class ArrayNew { public static void main(String[] args) { int[] a; Random rand = new Random(47); a = new int[rand.nextInt(20)]; }}
如果建立了一個非基本類型的數組,那麼就是一個引用數組。以整型的封裝器類Integer為例:
public class Test { public static void main(String[] args) { Random rand = new Random(47); Integer[] a = new Integer[rand.nextInt(20)]; }}
這裡即便用new建立了數組之後,也只是一個引用數組,並且直到通過建立新的Integer對象,並把對象和引用串連起來,初始化才算結束。如果忘記了建立對象,並連結化物件和引用,那數組中就全是Null 參考,運行時會產生異常。
5.8.1 可變參數列表
可變參數列表可用於參數個數或者類型未知的場合。例如void f(Object[] args)函數裡的參數。這種在Java SE5之前出現,然而再Java SE5中,添加入了新特性,可以使用新特性來定義可變參數列表了,下面的例子:
public class NewVarArgs { static void printArray(Object... args) { for(Object obj : args) { System.out.println(obj + " "); } } public static void main(String[] args) { printArray(new Integer(47), new Float(3.14), new Double(11.11)); printArray(47, 3.14F, 11.11); printArray("one", "two", "three"); }}
有了可變參數,就不用顯示的編寫數組文法了,當指定參數的時候,編譯器實際上會去填充數組,最後得到的仍然是一個數組。
5.9 枚舉類型
Java SE5中添加了一個看似很小的特性,即enum關鍵字,它使得我們在需要群組並使用枚舉集時可以很方便的處理。C和C++以及以其他很多語言已經擁有枚舉類型了,Java中枚舉類型功能比C/C++的功能更加完備。下面是簡單的樣本:
public enum A { NOT, MILD, MEDIUM, HOT, FLAMING}
這裡建立了一個名為A的枚舉類型,它具有5種值,由於枚舉類型的執行個體是常量,因此按照命名習慣通常用大寫字母表示(有多個字母用底線隔開)。
為了使用enum,需要建立一個該類型的引用,並和某個執行個體串連起來。
public class Test { public static void main(String[] args) { A a = A.MEDUIM; System.out.println(a); }}
在建立枚舉類型的時候,編譯器會自動添加一些特性。例如:
- 會建立toString()方法,一邊可以很方便的顯示某個enum執行個體的名字。
- 建立ordinal()方法,用來表示某個特定enum常量的聲明順序。
- static values()方法,用來按照enum常量的聲明順序,產生由這些常量值構成的數組。
例子如下:
public class EnumOrder { public static void main(String[] args){ for(A a : A.values) { System.out.println(s + ", oridinal " + a.ordinal()); } }}輸出結果為:NOT, oridinal 0MILD, oridinal 1MEDIUM, oridinal 2HOT, oridinal 3FLAMING, oridinal 4
enum的另一個特別實用的特性是能和switch語句一起使用。看下面的例子:
enum Pet { Cat, Dog, Bird}public class JavaTest { Pet pet; public JavaTest(Pet p) { pet = p; } public void describe() { switch(pet) { case Cat : System.out.println("The pet is Cat"); break; case Dog : System.out.println("The pet is Dog"); break; case Bird : System.out.println("The pet is Bird"); break; } } public static void main(String[] args) { Pet p1 = Pet.Bird; JavaTest test = new JavaTest(p1); test.describe(); }}結果為:The pet is Bird
Thinking In Java筆記(第五章 初始化與清理(三))