《 Java 編程思想》CH05 初始化與清理

用構造器確保初始化

在 Java 中,經過提供構造器,類的設計者可確保每一個對象都會獲得初始化。Java 會保證初始化的進行。構造器採用與類相同的名稱java

  • 因爲構造器的名稱必須與類名徹底相同,因此「每一個方法首字母小寫」的風格在這裏不適用。
  • 構造器方法能夠有參數,這樣能夠在初始化對象時提供實際參數。
  • 不接受任何參數的構造器叫作「默認構造器」。
  • 構造器一種特殊類型的方法,它沒有返回值

方法重載

由於能夠要用多種方式來建立並初始化一個對象,因此就須要多個構造器,而構造器的名稱又須要和類名相同,因此必須容許方法名相同而形式參數不一樣的構造器存在,因此 Java 中有方法重載算法

class Rock {
    Rock() {
        // 默認構造器
    }
    Rock(int i) {
        // 帶參數的構造器
        System.out.println("i=" + i);
    }
    void print() {
        System.out.println("i = null");
    }
    void print(int i) {
        System.out.println("i = " + i);
    }
}

// 初始化
Rock r1 = new Rock(); // 調用默認構造器
Rock r2 = new Rock(1); // 調用帶參數的構造器
r2.print();
r2.print(1);

區分重載方法

方法簽名是由方法名和參數類型列表構成的,因此用參數類型列表區分重載方法。不能用返回值區分重載方法數組

設計基本類型的重載

因爲基本類型可能會從一個「較小」的類型自動提高爲一個「較大」的類型,因此在重載方法中須要特別注意:dom

  • 常數值看成int處理
  • 實參的數據類型小於形參的數據類型時,會自動提高
  • 對於 char 類型,若是找不到以 char 做爲形參的方法的話,會把 char 類型提高成 int 類型
  • 若是實參大於形參的話,須要顯式的強制轉換不然會報編譯錯誤

默認構造器

  • 默認構造器是一個沒有形式參數的構造器,其做用是建立一個「默認對象」
  • 若是類中沒有構造器,編譯器會自動建立一個默認構造器
  • 若是類中已經有了一個構造器了,編譯器則不會自動建立默認構造器

this 關鍵字

因爲同一類型的對象均可以調用相同的方法,爲了在方法中區分不一樣的對象,會把對象的引用做爲參數傳遞給方法,a.fun(1)在編譯器內部會被翻譯成ClassName.fun(a, 1),咱們能夠經過this關鍵字在方法中訪問到對象的引用。函數

  • 在方法內部調用同個類的另外一個方法不須要使用this,直接調用便可。
  • this 能夠在方法內部做爲參數傳遞給另外一個方法,也能夠做爲返回值(能夠構造出鏈式操做)

在構造器中調用構造器

  • 能夠利用 this 來實如今構造器中調用構造器,這樣能夠避免重複代碼。
  • this 在一個構造器中只能調用一次構造器
  • 必須將構造器置於最開始處,不然編譯器會報錯
public class Flower {
    int petalCount = 0;
    String s = "initial value";

    Flower(int petals) {
        petalCount = petals;
        System.out.println("int arg ,petalCount = " + petalCount);
    }

    Flower(String ss) {
        s = ss;
        System.out.println("string arg ,s = " + s);
    }

    Flower(String s, int petals) {
        this(petals);
        // this 只能調用一次構造器
        this.s = s;
        System.out.println("string & int arg, s = " + s + ", petalCount = " + petalCount);
    }

    Flower() {
        this("hello", 24);
    }

    public static void main(String[] args) {
        Flower flower = new Flower();
        System.out.println("flower.petalCount = " + flower.petalCount);
        System.out.println("flower.s = " + flower.s);
    }
}
// int arg ,petalCount = 24
// string & int arg, s = hello, petalCount = 24
// flower.petalCount = 24
// flower.s = hello

static 的含義

static 方法就是沒有 this 的方法,在 static 中不能調用非靜態方法,可是反過來能夠。ui

清理:終結處理和垃圾回收

  1. 對象可能不會被垃圾回收
  2. 垃圾回收不等於「析構」this

    1. Java 未提供「析構函數」或類似的概念,要作相似的清理工做,必須手動建立一下執行清理工做的普通方法
  3. 垃圾回收只與內存有關lua

    1. 對與垃圾回收有關的任何行爲來講(尤爲是 finalize() 方法),它們必須同內存及其回收相關
    2. finalize() 的需求一般是用於一種特殊狀況,即經過某種建立對象方式之外的方式爲對象分配了存儲空間,這種狀況主要發生在使用了「本地方法」的狀況下,本地方法是一種在 Java 中調用非 Java 代碼的方式。如在非 Java 代碼中調用了 malloc,爲了釋放內存,咱們須要在 finalize() 中調用對應的本地方法進行 free。
    3. finalize() 方法的執行機制:一旦垃圾回收器準備釋放對象佔用的存儲空間,首先調用其 finalize() 方法,而且在下一次垃圾回收動做發生時,纔會真正回收對象佔用的內存。

你必須實施清理

Java 中沒有用於釋放對象的 delete,由於垃圾回收器會自動幫你釋放存儲空間,所以 Java 中沒有析構函數。可是垃圾回收不能徹底代替析構函數,若是但願進行除釋放存儲空間以外的清理工做,咱們須要明確調用某個 Java 方法。例如某個類打開了一個文件,垃圾回收不能自動幫咱們關閉這個文件。爲何這個工做不能有 finalize() 方法來完成呢,緣由其實在上面已經說明了,對象可能不會被垃圾回收,也就是說 finalize() 方法可能永遠都不會被調用。翻譯

若是 JVM 沒有面臨內存耗盡的狀況,它是不會浪費時間去執行垃圾回收以恢復內存的。設計

終結條件

雖然咱們不能用 finalize() 方法來進行「清理」,可是咱們能夠利用它驗證某個對象的終結條件。仍是剛纔那個打開文件的例子,假設在文件沒有關閉的時候,垃圾回收將對象回收了,這就會產生一些很是難找的 bug。而 finalize() 能夠幫助咱們發現這種 bug。

class Book {
    boolean checkedOut = false;
    Book(boolean checkOut) {
        checkedOut = checkOut;
    }
    void checkIn() {
        checkedOut = false;
    }

    protected void finalize() {
        if (checkedOut) {
            System.out.println("Error: checked out");
        }
        // super.finalize();
    }
}

public class TerminationCondition {
    public static void main(String[] args) {
        Book novel = new Book(true);
        novel.checkIn();
        new Book(true);
        System.gc();
    }
}
// Error: checked out

如上面這個例子,咱們但願 Book 在被回收前已經 checkIn 了,因此咱們在 finalize() 中寫了一個條件語句來判斷。

  • System.gc()強制 GC
  • 應該老是假設基類的 finalize() 也須要作某些時間,因此咱們應該在 finalize() 函數的末尾加入 super.finalize();

垃圾回收器如何工做

垃圾回收器會提升對象在堆上建立的速度,這是由於 Java 的堆的實現與 C++ 的不一樣,其更像是一個傳送帶,每分配一個對象,它就往前移動一格,因此「堆指針」 只是簡單的移動到還沒有分配的空間,這意味 Java 中在堆上的分配速度很是快。固然,若是隻是簡單的像傳送帶同樣工做的話,Java 的堆會佔用大量的虛擬內存,進而致使頻繁的頁面調度,並可能會致使內存資源耗盡,所以須要有垃圾回收器的介入。垃圾回收會一邊回收空間一邊對堆進行「緊湊」操做。

幾種常見的垃圾回收機制:

  • 引用計數:一種簡單但比較慢的垃圾回收機制。

    • 每一個對象都有一個引用計數器,當引用鏈接對象時,引用計數加一,當引用離開做用域或被置 null 時,引用計數減一。
    • 這種方法沒法處理「循環引用」的狀況。
  • 中止-複製(stop-and-copy):

    • 其依據的思想是:對任何「活」的對象,必定能最終追溯到其存活在堆棧或靜態存儲區域之中。所以,能夠從堆棧和靜態存儲區開始,遍歷全部的引用,並遞歸查找該對象所包含的全部對象,便可找到全部「活」的對象。
    • 該機制會先暫停程序的運行,而後將全部存活的對象從當前堆複製到另外一個堆,而後更新引用
    • 當對象被複制到新的堆中時,沒複製的則至關於被回收了,同時能夠實現「緊湊」的目標。
    • 該機制會有如下兩個缺點:

      • 須要兩個堆,進而須要維護比以前大一倍的空間
      • 若是隻有少許垃圾甚至沒有垃圾,而這時若是進行垃圾回收的話,開銷太大了。
  • 標記-清掃(mark-and-sweep):

    • 與 stop-and-copy 機制依據的思想是同樣的也是,用一樣的方式找到「活」的對象
    • 每當它找到一個「活」的對象,就會給該對象一個標記,這個過程當中不會回收任何對象。只有當所有標記工做完成時,纔會進行清理。
    • 清理的過程當中,沒有被標記的對象被釋放,但不會作任何複製動做
    • 爲了不存儲空間的「碎片化」,JVM 須要作緊湊操做

JVM 中採用的垃圾回收機制:

  • 一種結合的 stop-and-copy 和 mark-and-sweep 的自適應垃圾回收算法
  • 內存分配以較大的「快」做爲單位,較大的對象能夠獨佔一個塊。每一個塊都有相應的代數(generation count)來記錄它是否存活。
  • 垃圾回收會對上次回收操做以後新分配的快進行整理,這樣有助於解決有大量短命對象的狀況。
  • 垃圾回收機制會按期進行完整的清理——大型對象仍然不會被複制(只是其代數會增長),而那些含有大量小型對象的快會被複制並整理。
  • 當只有少許或沒有垃圾產生時,則轉爲使用 mark-and-sweep 算法。

Java 中 JIT(Just-In-Time)技術:

這種技術能夠把程序所有或部分翻譯成本地機器碼,而不是經過 JVM,進而提高程序的運行速度。

當須要裝載某個類時(第一次建立這個類時),編譯器會找到其.class 文件,而後將該類的字節碼裝入內存,此時有兩種作法:

  • JIT 直接編譯全部代碼,但這個作法會有兩個缺點:

    • 加載動做分散在整個程序中,累加起來要話更多時間
    • 可能會增長可執行代碼的長度,進而致使頁面調度
  • 惰性評估(lazy evaluation):,即 JIT 只在必要時才編譯,這樣不會執行的代碼就不會被 JIT 所編譯。

成員初始化

Java 盡力保證:全部變量在使用前都能獲得適當的初始化。

局部變量沒有默認初始值,若有在未初始化前使用它會報錯編譯錯誤,而類變量則有默認初始值。

指定初始化

Java 容許在定義類成員變量的時候爲其賦值進行初始化。非基本類型也能夠,同時可使用已經函數或已經初始化好的變量進行初始化,但要保證初始化順序的正確。

public class InitialValues {

    boolean t = false;
    char c = 'a';
    byte b = 1;
    short s = 2;
    int i = func(s);
    long l = 4 + i;
    float f = (float)5.0; // 浮點數字面量是 double 類型的
    double d = 6.0;
    String reference = new String("hello world"); // 非基本類型也能夠

    int func(short s) {
        return s*2;
    }
}

構造器初始化

  • 沒法阻止自動初始化的進行,它將在構造器以前執行。
  • 類變量的定義順序決定了初始化的順序
  • 靜態數據的默認值與類變量一致
  • 對於靜態變量,Java 能夠將多個初始化語句組合成一個靜態塊,其和靜態變量初始化同樣在類加載時執行。順序與定義時的順序相同
  • 對於非靜態變量,Java 中也能夠將多個初始化語句組成一個塊,在實例初始化執行。
  • 對於以上兩種塊,既能夠能夠把它當成一條初始化語句來看待。
public class InitialValues {

    boolean t = false;
    char c = 'a';
    byte b = 1;
    short s = 2;
    int i = 3;
    long l = 4;
    float f;
    double d;
    String reference;

    {
        f = (float) 1.0;
        d = 2*f;
        reference = new String("hello");
        reference = reference + f + d;
    }

    static {
        System.out.println("hello");
    }
    static int a;
    static {
        System.out.println("A is " + a);
        a = 2;
    }
}

對象的建立過程:

  1. 第一次建立類或者訪問其靜態數據或方法,JVM 會加載其 .class 文件,此時執行全部靜態初始化(按定義的順序執行)。
  2. 當 new 該類時,首先會在堆上分配空間,由於堆在分配前被置零了,因此本類型的默認值都是 0,非基本類型的引用的默認值則是 null。
  3. 按順序執行非靜態的初始化
  4. 執行構造器

數組初始化

數組是同類型的、用一個標識符名稱封裝到一塊兒的一個對象序列或基本類型數據序列。

定義方式:

int[] a; //建議使用這種
int a[]; //這樣也能夠, 可是不能指定數組的類型。`int a[3];` 這樣是不容許的

int[] a;這樣只是定義了一個數組的引用,咱們可使用new來建立一個數組,也能夠直接初始化數組:

int[] a = new int[3]; // 使用 new 來建立一個數組,這時真實數據會分配在堆中,因此默認值都爲「零」
int[] b = {1, 2, 3}; // 直接初始化一個長度爲3的數組
Integer[] c = new Integer[3]; // 建立一個對象數組,保存引用,這時初始值都爲 null
Random rand = new Random(2);
int len = rand.nextInt(20);
int[] c = new int[len]; // 長度不必定要是一個字面值,能夠是變量

數組初始化的坑點:

InitialValues initialValues = new InitialValues();
initialValues.printInitialValues();
String[] stringArray = {"hello", "world"};
// initialValues.printStringArrary({"hello", "world"}); // 編譯錯誤
initialValues.printStringArrary(stringArray);
initialValues.printStringArrary(new String[]{"hello", "world"}); //正確打開方式
int[] intArray = {1, 2, 3, 4};
// initialValues.printIntArray({1, 2, 3, 4}); // 編譯錯誤
initialValues.printIntArray(intArray);
initialValues.printIntArray(new int[]{1, 2, 3, 4});

可變參數列表

在方法中,用ClassName... ArgName的形式能夠定義可變參數列表,在方法中,ArgName 本質上是一個數組。在可變列表中可使用任何類型,包括基礎類型。這裏傳入基本類型時,沒有依賴自動裝包和解包,這意味着,ClassName 爲 int 時,ArgName 是一個 int[],而不是 Integer。在重載方法時,應該只在一個方法中使用可變參數列表

static void printArray(Object... args) {
    for(Object arg: args) {
        System.out.print(arg + " ");
    }
    System.out.println();
}

static void f(int required, int... args) {
    System.out.println("Required: " + required);
    for(int i: args) {
        System.out.print(i + " ");
    }
    System.out.println();
}

public static void main(String[] args) {
    printArray(1, 2, 3, 4, 5);
    f(1);
    f(1, 2, 3);
    Integ
}

枚舉類型

  • 按照命名習慣,枚舉值通常用全大寫字母
  • 爲了使用 enum,須要建立一個該類型的引用
  • enum 會自動建立一些實用的函數,如toString()顯示其名稱,ordinal()表示聲明順序
  • enum 適合與 switch 一塊兒使用
enum EnumDemo {
    HELLO, WORLD, 
};

EnumDemo e1 = EnumDemo.HELLO;
System.out.println(e1); // 自動調用toString()
System.out.println(e1.ordinal());

for(EnumDemo e: EnumDemo.values()) {
    System.out.println("EnumDemo: " + e + " ordinal " + e.ordinal());
}
// HELLO
// 0
// EnumDemo: HELLO ordinal 0
// EnumDemo: WORLD ordinal 1
相關文章
相關標籤/搜索