Thinking In Java筆記(第五章 初始化與清理(三))

第五章 初始化與清理

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的類:

  1. 即使沒有顯式的使用static關鍵字,構造器實際上也是靜態方法。因此,當首次創建類型爲Dog對象時(構造器可以看成靜態方法),或者Dog類的靜態方法、靜態域首次被訪問的時候,Java解釋器必須查找類的路徑,以定位Dog.class。
  2. 然後載入Dog.class,有關靜態初始化的所有動作都會被執行,因此,靜態初始化只在Class對象首次加載的時候進行一次。
  3. 當用new創建Dog的對象時候,首先在堆上給Dog對象分配足夠的存儲空間。
  4. 清零所分配的存儲空間,這就自動的將Dog對象中所有的基本數據類型設置成了默認值(對數字來說就是0,對boolean和char類型的來說也類似),而引用則都被設置成了null。
  5. 執行所有出現於字段定時的初始化動作。
  6. 執行構造器方法。

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