第五章 初始化與清理
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對象中所有的基本數據類型設置成了默認值(對數字來說就是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 - Cup
Block - Cup
Construct - Cup(1)
Block - Cup
Construct - 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對象,並把對象和引用連接起來,初始化纔算結束。如果忘記了創建對象,並鏈接對象和引用,那數組中就全是空引用,運行時會產生異常。
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 0
MILD, oridinal 1
MEDIUM, oridinal 2
HOT, oridinal 3
FLAMING, 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