Java編程思想學習(五)----第5章:初始化與清理

来源:https://www.cnblogs.com/zhongli1/archive/2018/12/02/10029822.html
-Advertisement-
Play Games

隨著電腦革命的發展,“不安全”的編程方式已逐漸成為編程代價高昂的主因之一。 C++引入了構造囂(constructor)的概念,這是一個在創建對象時被自動調用的特殊方法。Java中也採用了構造器,並額外提供了“垃圾回收器”。對於不再使用的記憶體資源,垃圾回收器能自動將其釋放。 5.1 用構造器確保初 ...


隨著電腦革命的發展,“不安全”的編程方式已逐漸成為編程代價高昂的主因之一。

C++引入了構造囂(constructor)的概念,這是一個在創建對象時被自動調用的特殊方法。Java中也採用了構造器,並額外提供了“垃圾回收器”。對於不再使用的記憶體資源,垃圾回收器能自動將其釋放。

5.1 用構造器確保初始化

//:initialization/SimpleConstructor.java
//Demonstration of a simple constructor.
class Rock 
{
    Rock()
    {
        System.out.print("Rock ");
    }
}
public class SimpleConstructor
{
    public static void main(String[] args)
    {
        for (int i = 0; i < 10; i++)
        {
            new Rock();
        }
    }
}/*Output
    Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock 
    *///:~

  在創建對象時:new Rock();將會為對象分配存儲空間,並調用相應的構造器。這就確保了在你能操作對象之前,它已經被恰當地初始化了。

  請註意,由於構造器的名稱必須與類名完全相同,所以“每個方法首字母小寫”的編碼風格並不適用於構造器。

//:initialization/SimpleConstructor2.java
class Rock2 
{
    Rock2(int i)
    {
        System.out.print("Rock2 "+i+" ");
    }
}
public class SimpleConstructor2
{
    public static void main(String[] args)
    {
        for (int i = 0; i < 8; i++)
        {
            new Rock2(i);
        }
    }
}/*Output
    Rock2 0 Rock2 1 Rock2 2 Rock2 3 Rock2 4 Rock2 5 Rock2 6 Rock2 7  
    *///:~

  有了構造器形式參數,就可以在初始化對象時提供實際參數。例知,假設類Tree有一個構造器,它接受一個整型變數來表示樹的高度,就可以這樣創建一個Tree對象:

Tree t = new Tree(12); //12-foot tree

  如果Tree(int)是Tree類中唯一的構造器,那麼編譯器將不會允許你以其他任何方式創建Tree對象。

  構造器有助於減少錯誤,並使代碼更易於閱讀。從概念上講,“初始化”與“創建”是彼此獨立的,然而在上面的代碼中,你卻找不到對initialize()方法的明確調用。在Java中,“初始化”和“創建”捆綁在一起,兩者不能分離。

  • 練習1:(1)創建一個類,它包含一個未初始化的String引用。驗證該引用被Java初始化成了null。
  • 練習2:(2)創建一個類,它包含一個在定義時就被初始化了的String域,以及另一個通過構造器初始化的String域。這兩種方式有何差異?

5.2 方法重載

  當創建一個對象時,也就給此對象分配到的存儲空間取了一個名字。所謂方法則是給某個動作取的名字。

  大多數程式設計語言(尤其是C)要求為每個方法(在這些語言中經常稱為函數)都提供一個獨一無二的標識符。

  所以絕不能用名為print()的函數顯示了整數之後,又用一個名為print()的函數顯示浮點數——每個函數都要有唯一的名稱。

   構造器是強制重載方法名的另一個原因。既然構造器的名字已經由類名所決定,就只能有一個構造器名。那麼要想用多種方式創建一個對象該怎麼辦呢?假設你要創建一個類,既可以用標準方式進行初始化,也可以從文件里讀取信息來初始化。這就需要兩個構造器:一個預設構造器,另一個取字元串作為形式參數——該字元串表示初始化對象所需的文件名稱。由於都是構造器,所以它們必須有相同的名字,即類名。為了讓方法名相同而形式參數不同的構造器同時存在,必須用到方法重載。

下麵這個例子同時示範了重載的構造器和重載的方法:

class Tree
{
   int height;

   public Tree()
   {
       height = 0;
       System.out.println("種植樹苗");
   }

   public Tree(int initialHeight)
   {
       height = initialHeight;
       System.out.println("新創建了一顆 " + height + " 高的樹");
   }

   void info()
   {
       System.out.println("本樹高為 " + height);
   }

   void info(String s)
   {
       System.out.println(s + ":本樹高為 " + height);
   }
}

public class Overloading 
{
    public static void main(String[] args)
    {
        for (int i = 0; i < 3; i++)
        {
            Tree t = new Tree(i);
            t.info();
            t.info("重載的方法");
        }
        //重載構造器
        new Tree();
    }
}/*Output
    新創建了一顆 0 高的樹
    本樹高為 0
    重載的方法:本樹高為 0

    新創建了一顆 1 高的樹
    本樹高為 1
    重載的方法:本樹高為 1

    新創建了一顆 2 高的樹
    本樹高為 2
    重載的方法:本樹高為 2

    種植樹苗
    *///:~

5.2.1 區分重載方法

規則很簡單:每個重載的方法都必須有一個獨一無二的參數類型列表。

甚至參數順序的不同也足以區分兩個方法。不過,一般情況下別這麼做,因為這會使代碼 
難以維護:

public class OverloadingOrder
{
    static void f(String s, int i)
    {
        System.out.println("String: " + s + ", int: " + i);
    }

    static void f(int i, String s)
    {
        System.out.println("int: " + i + ", String: " + s);
    }

    public static void main(String[] args)
    {
        f("String first", 11);
        f(99, "int first");
    }
}/*Output
    String: String first, int: 11
    int: 99, String: int first
    *///:~

上例中兩個f()方法雖然聲明瞭相同的參數,但順序不同,因此得以區分。

5.2.2 涉及基本類型的重載

  基本類型能從一個“較小一的類型自動提升至一個“較大”的類型,此過程一旦牽涉到重載,可能會造成一些混淆。以下例子說明瞭將基本類型傳遞給重載方法時發生的情況:

public class PrimitiveOverloading
{
    //*******************f1***************//
    void f1(char x)
    {
        System.out.print("f1(char) ");
    }

    void f1(byte x)
    {
        System.out.print("f1(byte) ");
    }

    void f1(short x)
    {
        System.out.print("f1(short) ");
    }

    void f1(int x)
    {
        System.out.print("f1(int) ");
    }

    void f1(long x)
    {
        System.out.print("f1(long) ");
    }

    void f1(float x)
    {
        System.out.print("f1(float) ");
    }

    void f1(double x)
    {
        System.out.print("f1(double) ");
    }

    //********************f2**************//
    void f2(byte x)
    {
        System.out.print("f2(byte) ");
    }

    void f2(short x)
    {
        System.out.print("f2(short) ");
    }

    void f2(int x)
    {
        System.out.print("f2(int) ");
    }

    void f2(long x)
    {
        System.out.print("f2(long) ");
    }

    void f2(float x)
    {
        System.out.print("f2(float) ");
    }

    void f2(double x)
    {
        System.out.print("f2(double) ");
    }

    //*******************f3***************//
    void f3(short x)
    {
        System.out.print("f3(short) ");
    }

    void f3(int x)
    {
        System.out.print("f3(int) ");
    }

    void f3(long x)
    {
        System.out.print("f3(long) ");
    }

    void f3(float x)
    {
        System.out.print("f3(float) ");
    }

    void f3(double x)
    {
        System.out.print("f3(double) ");
    }

    //********************f4***************//
    void f4(int x)
    {
        System.out.print("f4(int) ");
    }

    void f4(long x)
    {
        System.out.print("f4(long) ");
    }

    void f4(float x)
    {
        System.out.print("f4(float) ");
    }

    void f4(double x)
    {
        System.out.print("f4(double) ");
    }

    //********************f5**************//
    void f5(long x)
    {
        System.out.print("f5(long) ");
    }

    void f5(float x)
    {
        System.out.print("f5(float) ");
    }

    void f5(double x)
    {
        System.out.print("f5(double) ");
    }
    //********************f6**************//

    void f6(float x)
    {
        System.out.print("f6(float) ");
    }

    void f6(double x)
    {
        System.out.print("f6(double) ");
    }

    //********************f7**************//
    void f7(double x)
    {
        System.out.print("f7(double) ");
    }

    void testConstVal()
    {
        System.out.print("5: ");
        f1(5);
        f2(5);
        f3(5);
        f4(5);
        f5(5);
        f6(5);
        f7(5);
        System.out.println();
    }

    void testChar()
    {
        char x = 'x';
        System.out.print("char: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    void testByte()
    {
        byte x = 0;
        System.out.print("byte: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    void testShort()
    {
        short x = 0;
        System.out.print("short: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    void testInt()
    {
        int x = 0;
        System.out.print("int: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    void testLong()
    {
        long x = 0;
        System.out.print("long: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    void testFloat()
    {
        float x = 0;
        System.out.print("float: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    void testDouble()
    {
        double x = 0;
        System.out.print("double: ");
        f1(x);
        f2(x);
        f3(x);
        f4(x);
        f5(x);
        f6(x);
        f7(x);
        System.out.println();
    }

    public static void main(String[] args)
    {
        PrimitiveOverloading p = new PrimitiveOverloading();
        p.testConstVal();
        p.testChar();
        p.testByte();
        p.testShort();
        p.testInt();
        p.testLong();
        p.testFloat();
        p.testDouble();
    }
}/*Output
    5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 
    char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 
    byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double) 
    short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double) 
    int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 
    long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double) 
    float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double) 
    double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double) 
    *///:~

  你會發現常數值5被當作int值處理,所以如果有某個重載方法接受int型參數,它就會被調用。至於其他情況,如果傳入的數據類型(實際參數類型)小於方法中聲明的形式參數類型,實際數據類型就會被提升。 char型略有不同,如果無法找到恰好接受char參數的方法,就會把char直接提升至int型。

  如果傳入的實際參數大於重載方法聲明的形式參數,會出現什麼情況呢?修改上述程式,就能得到答案。

public class Demotion()
{  //*******************f1***************//
    void f1(char x)
    {
        System.out.print("f1(char) ");
    }

    void f1(byte x)
    {
        System.out.print("f1(byte) ");
    }

    void f1(short x)
    {
        System.out.print("f1(short) ");
    }

    void f1(int x)
    {
        System.out.print("f1(int) ");
    }

    void f1(long x)
    {
        System.out.print("f1(long) ");
    }

    void f1(float x)
    {
        System.out.print("f1(float) ");
    }

    void f1(double x)
    {
        System.out.print("f1(double) ");
    }

    //********************f2**************//
    void f2(char x)
    {
        System.out.print("f2(char) ");
    }

    void f2(byte x)
    {
        System.out.print("f2(byte) ");
    }

    void f2(short x)
    {
        System.out.print("f2(short) ");
    }

    void f2(int x)
    {
        System.out.print("f2(int) ");
    }

    void f2(long x)
    {
        System.out.print("f2(long) ");
    }

    void f2(float x)
    {
        System.out.print("f2(float) ");
    }


    //*******************f3***************//
    void f3(char x)
    {
        System.out.print("f3(char) ");
    }

    void f3(byte x)
    {
        System.out.print("f3(byte) ");
    }

    void f3(short x)
    {
        System.out.print("f3(short) ");
    }

    void f3(int x)
    {
        System.out.print("f3(int) ");
    }

    void f3(long x)
    {
        System.out.print("f4(long) ");
    }

    //********************f4***************//
    void f4(char x)
    {
        System.out.print("f4(char) ");
    }

    void f4(byte x)
    {
        System.out.print("f4(byte) ");
    }

    void f4(short x)
    {
        System.out.print("f4(short) ");
    }

    void f4(int x)
    {
        System.out.print("f4(int) ");
    }

    //********************f5**************//
    void f5(char x)
    {
        System.out.print("f5(char) ");
    }

    void f5(byte x)
    {
        System.out.print("f5(byte) ");
    }

    void f5(short x)
    {
        System.out.print("f5(short) ");
    }

    //********************f6**************//

    void f6(char x)
    {
        System.out.print("f6(char) ");
    }

    void f6(byte x)
    {
        System.out.print("f6(byte) ");
    }

    //********************f7**************//
    void f7(char x)
    {
        System.out.print("f7(char) ");
    }


    void testDouble()
    {
        double x = 0;
        System.out.print("double argument: ");
        f1(x);
        f2((float) x);
        f3((long) x);
        f4((int) x);
        f5((short) x);
        f6((byte) x);
        f7((char) x);
        System.out.println();
    }

    public static void main(String[] args)
    {
        Demotion d = new Demotion();
        d.testDouble();
    }
}/*Output
    double argument: f1(double) f2(float) f4(long) f4(int) f5(short) f6(byte) f7(char) 
    *///:~

  在這裡,方法接受較小的基本類型作為參數。如果傳入的實際參數較大,就得通過類型轉換來執行窄化轉換。如果不這樣做,編譯器就會報錯。

5.2.3 以返回值區分重載方法

void f()
{
}

int f() 
{ 
    return 1; 
}
f();

  此時Java如何才能判斷該調用哪一個f()呢?別人該如何理解這種代碼呢?因此,根據方法的返回值來區分重載方法是行不通的。

5.3 預設構造器

  如果你寫的類中沒有構造器,則編譯器會自動幫你創建一個預設構造器。

//: initialization/DefaultConstructor.java
class Bird 
{
}

public class DefaultConstructor 
{
    public static void main (String[] args) 
    {
        Brid b = new Bird();//Default
    }
}///:~

  表達式 new Bird()行創建了一個新對象,並調用其預設構造器——即使你沒有明確定義它。沒有它的話,就沒有方法可調用,就無法創建對象。但是,如果已經定義了一個構造器(無論是否有參數),編譯器就不會幫你自動創建預設構造器:

//: initialization/NoSynthesis.java
class  bird2  
{
    Bird2(int i) 
    {
    }

    Bird2(double d) 
    {
    }
}

public class NoSynthesis
{
    public staticvoid main(String[] args)
    {
        //! Bird2 b = new Bird2(); // No default
        Bird2 b2=new Bird2(1);
        Bird2 b3=new Bird2(1.0);
    }
 }  ///:~

  要是你這樣寫:new Bird2()編譯器就會報錯:沒有找到匹配的構造器。

  • 練習3:(1)創建一個帶預設構造器(即無參構造器)的類,在構造器中列印一條消息。為這個類創建一個對象。
  • 練習4:(1)為前一個練習中的類添加一個重載構造器,令其接受一個字元串參數,併在構造器中把你自己的消息和接收的參數一起列印出來。
  • 練習5:(2)創建一個名為Dog的類,它具有重載的bark()方法。此方法應根據不同的基本數據類型進行重載,並根據被調用的版本,列印出不同類型的狗吠(barking)、咆哮(howling)等信息。編寫main()來調用所有不同版本的方法。
  • 練習6:(1)修改前一個練習的程式,讓兩個重載方法各自接受兩個類型的不同的參數,但二者順序相反。驗證其是否工作。
  • 練習7:(1)創建一個沒有構造器的類,併在main()中創建其對象,用以驗證編譯器是否真的自動加入了預設構造器。

5.4 this關鍵字

  如果有同一類型的兩個對象,分別是a和b。你可能想知道,如何才能讓這兩個對象都能調用peel()方法呢:

public class BananaPeel  
{
    public static void main(String[] args)I
    {
        Banana a = new Banana(),b = new Banana();
        a.peel(1);
        b.peel(2);
    }
}///:~

  如果只有一個peel()方法,它如何知道是被a還是被b所調用的呢?

  它暗自把“所操作對象的引用”作為第一個參數傳遞給peel()。所以上述兩個方法的調用就變成了這樣:

Banana.peel(a,1);
Banana.peel(b,2);

  this關鍵字只能在方法內部使用,表示對“調用方法的那個對象”的引用。this的用法和其他對象引用並無不同。但要註意,如果在方法內部調用同一個類的另一個方法,就不必使用this,直接調用即可。當前方法中的this引用會自動應用於同一類中的其他方法。所以可以這樣寫代碼:

public class Apricot 
{
    void pick ()
    {
        /*...*/
    }

    void pit()
    {
        pick();
        /*...*/
    }
}///:~

  在pit()內部,你可以寫this.pick(),但無此必要。編譯器能幫你自動添加。只有當需要明確指出對當前對象的引用時,才需要使用this關鍵字。例如,當需要返回對當前對象的引用時,就常常在return語句里這樣寫:

public class Leaf
{
    int i = 0;
    Leaf increment()
    {
        i++;
        return this;
    }

    void print()
    {
        System.out.println("i = "+i);   
    }

    public static void main(String[] args)
    {
        Leaf l = new Leaf();
        l.increment().increment().increment().print();
    }
}/*Output
    i = 3
    ///:~

  由於increment()通過this關鍵字返回了對當前對象的引用,所以很容易在一條語句里對同一個對象執行多次操作。

  this關鍵字對於將當前對象傳遞給其他方法也很有用:

class Person
{
    public void eat(Apple apple)
    {
        Apple peeled = apple.getPeeled();
        System.out.println("Yummy");
    }
}

class Peeler
{
    static Apple peel(Apple apple)
    {
        //...remove peel
        return apple;
    }
}

class Apple
{
     Apple getPeeled()
     {
         return Peeler.peel(this);
     }
}

public class PassingThis 
{
    public static void main(String[] args)
    {
        new Person().eat(new Apple());
    }
}/*Output
    Yummy
    *///:~

  Apple需要調用Peeler.peel()方法,它是一個外部的工具方法,將執行由於某種原因而必須放在Apple外部的操作(也許是因為該外部方法要應用於許多不同的類,而你卻不想重覆這些代碼)。為了將其自身傳遞給外部方法,Apple必須使用this關鍵字。

  • 練習8:(1)編寫具有兩個方法的類,在第一個方法內調用第二個方法兩次:第一次調用時不使用this關鍵字,第二次調用時使用this關鍵字——這裡只是為了驗證它是起作用的,你不應該在實踐中使用這種方式。

5.4.1 在構造器中調用構造器

  可能為一個類寫了多個構造器,有時可能想在一個構造器中調用另一個構造器,以避免重覆代碼。可用this關鍵字做到達一點。

  通常寫this的時候,都是指“這個對象”或者“當前對象”,而且它本身表示對當前對象的引用。在構造器中,如果為this添加了參數列表,那麼就有了不同的含義。這將產生對符合此參數列表的某個構造器的明確調用,這樣,調用其他構造器就有了直接的途徑:

class Flower
{
    int petalCount = 0;
    String s = "initial value";

    Flower(int petals)
    {
        petalCount = petals;
        System.out.println("構造器 w/ int arg only,petalCount =" + petalCount);
    }

    Flower(String ss)
    {
        s = ss;
        System.out.println("構造器 w/ String arg only,s =" + s);
    }

    Flower(String s, int petals)
    {
        this(petals);
        //! this(s);//不能調用兩次構造器
        this.s = s;
        System.out.println("String and int arg");
    }

    Flower()
    {
        this("hi", 47);
        System.out.println("預設構造器(無參)");
    }

    void printPetalCount()
    {
        //! this(11); //不要在非構造方法里使用
        System.out.println("petalCount=" + petalCount + " s=" + s);
    }

     public static void main(String[] args)
    {
        Flower f = new Flower();
        f.printPetalCount();
    }
}/*Output
    構造器 w/ int arg only,petalCount =47
    String and int arg
    預設構造器(無參)
    petalCount=47 s=hi
    *///:~

  構造器Flower(String s,int petals)表明:儘管可以用this調用一個構造器,但卻不能調用兩個。此外,必須將構造器調用置於最起始處,否則編譯器會報錯。 這個例子也展示了this的另一種用法。由於參數s的名稱和數據成員s的名字相同,所以會產生歧義。使用this.s來代表數據成員就能解決這個問題。在Java程式代碼中經常出現這種寫法,本書中也常這麼寫。 printPetalCount()方法表明,除構造器之外,編譯器禁止在其他任何方法中調用構造器。 

  • 練習9:(1)編寫具有兩個(重載)構造器的類,併在第一個構造器中通過this調用第二個構造器。

5.4.2 static的含義

  在static方法的內部不能調用非靜態方法,反過來倒是可以的。(只會創建一次)

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

  Java有垃圾回收器負責回收無用對象占據的記憶體資源。但也有特殊情況:假定你的對象(並非使用new)獲得了一塊“特殊”的記憶體區域,由於垃圾回收器只知道釋放那些經由new分配的記憶體,所以它不知道該如何釋放該對象的這塊“特殊”記憶體。為了應對這種情況,Java允許在類中定義一個名為finalize()的方法。它的工作原理“假定”是這樣的:一旦垃圾回收器準備好釋放對象占用的存儲空間,將首先調用其finalize()方法,並且在下一次垃圾回收動作發生時,才會真正回收對象占用的記憶體。所以要是你打算用finalize(),就能在垃圾回收時刻做一些重要的清理工作。

  在C++中,對象一定會被銷毀(如果程式中沒有缺陷的話);而Java里的對象卻並非總是被垃圾回收。或者換句話說:

  1. 對象可能不被垃圾回收。
  2. 垃圾回收並不等於“析構”。
  3. 垃圾回收只與記憶體有關。

   Java並未提供“析構函數”或相似的概念,要做類似的清理工作,必須自己動手創建一個執行清理工作的普通方法。

  也許你會發現,只要程式沒有瀕臨存儲空間用完的那一刻,對象占用的空間就總也得不到釋放。如果程式執行結束,並且垃圾回收器一直都沒有釋放你創建的任何對象的存儲空間,則隨著程式的退出,那些資源也會全部交還給操作系統。這個策略是恰當的,因為垃圾回收本身也有開銷,要是不使用它,那就不用支付這部分開銷了。

5.5.1 finalize()的用途何在

  讀者或許已經明白了不要過多地使用finalize()的道理了 。對,它確實不是進行普通的清理工作的合適場所。那麼,普通的清理工作應該在哪裡執行呢?

5.5.2 你必須實施清理

  Java不允許創建局部對象,必須使用new創建對象。在Java中,也沒有用於釋放對象的delete,因為垃圾回收器會幫助你釋放存儲空間。甚至可以膚淺地認為,正是由於垃圾收集機制的存在,使得Java沒有析構函數。

  無論是“垃圾回收”還是“終結”,都不保證一定會發生。如果Java虛擬機(JVM)並未面臨記憶體耗盡的情形,它是不會浪費時間去執行垃圾回收以恢復記憶體的。

5.5.3 終結條件

  以下是個簡單的例子,示範了fifinalize()可能的使用方式:

 class Book
{
    boolean checkedOut = false;

    Book(boolean checkedOut)
    {
        this.checkedOut = checkedOut;
    }

    void checkIn()
    {
        checkedOut = false;
    }

    @Override
    protected void finalize() throws Throwable
    {
        if (checkedOut)
            System.out.println("ERROR: checked out");
        //通常情況下,你也會這麼做:
        super.finalize(); //調用基類方法
    }

    public static void main(String[] args)
    {
        Book novel = new Book(true);
        //適當的清理
        novel.checkIn();
        //作為參考,故意忘了清理
        new Book(true);
        //垃圾收集和終結
        System.gc();
    }
}/*Output
    ERROR: checked out
    *///:~

  System.gc()用於強制進行終結動作。即使不這麼做,通過重覆地執行程式(假設程式將分配大量的存儲空間而導致垃圾回收動作的執行),最終也能找出錯誤的book對象。

  • 練習10:(2)編寫具有finalize()方法的類,併在方法中列印消息。在main()中為該類創建一個對象。試解釋這個程式的行為。
  • 練習11:(4)修改前一個練習的程式,讓你的finalize()總會被調用。
  • 練習12:(4)編寫名為Tank的類,此類的狀態可以是“滿的”或“空的”。其終結條件是:對象被清理時必須處於空狀態。請編寫finalize()以檢驗終結條件是否成立。在main()中測試Tank可能發生的幾種使用方式。

5.5.4 垃圾回收器如何工作

  在以前所用過的程式語言中,在堆上分配對象的代價十分高昂,因此讀者自然會覺得Java中所有對象(基本類型除外),都在堆上分配的方式也非常高昂。然而,垃圾回收器對於提高對象的創建速度,卻具有明顯的效果。聽起來很奇怪——存儲空間的釋放竟然會影響存儲空間的分配,但這確實是某些Java虛擬機的工作方式。這也意味著,Java從堆分配空間的速度,可以和其他語言從堆棧上分配空間的速度相媲美。

  打個比方,你可以把C++里的堆想像成一個院子,裡面每個對象都負責管理自己的地盤。一段時間以後,對象可能被銷毀,但地盤必須加以重用。在某些Java虛擬機中,堆的實現截然不同:它更像一個傳送帶,每分配一個新對象,它就往前移動一格。這意味著對象存儲空間的分配速度非常快。Java的“堆指針”只是簡單地移動到尚未分配的區域,其效率比得上C++在堆棧上分配空間的效率。當然,實際過程中在簿記工作方面還有少量額外開銷,但比不上查找可用空間開銷大。

  讀者也許已經意識到了,Java中的堆未必完全像傳送帶那樣工作。要真是那樣的話,勢必會導致頻繁的記憶體頁面調度——將其移進移出硬碟,因此會顯得需要擁有比實際需要更多的記憶體。頁面調度會顯著地影響性能,最終,在創建了足夠多的對象之後,記憶體資源將耗盡。其中的秘密在於垃圾回收器的介入。當它工作時,將一面回收空間,一面使堆中的對象緊湊排列,這樣“堆指針”就可以很容易移動到更靠近傳送帶的開始處,也就儘量避免了頁面錯誤。通過垃圾回收器對對象重新排列,實現了一種高速的、有無限空間可供分配的堆模型。

  要想更好地理解Java中的垃圾回收,先瞭解其他系統中的垃圾回收機制將會很有幫助。引用記數是一種簡單但速度很慢的垃圾回收技術。每個對象都含有一個引用記數器,當有引用連接至對象時,引用計數加1。當引用離開作用域或被置為null時,引用計數減1。雖然管理引用記數的開銷不大,但這項開銷在整個程式生命周期中將持續發生。垃圾回收器會在含有全部對象的列表上遍歷,當發現某個對象的引用計數為0時,就釋放其占用的空間(但是,引用記數模式經常會在記數值變為0時立即釋放對象)。這種方法有個缺陷,如果對象之間存在迴圈引用,可能會出現“對象應該被回收,但引用計數卻不為零”的情況。對垃圾回收器而言,定位這樣的交互自引用的對象組所需的工作量極大。引用記數常用來說明垃圾收集的工作方式,但似乎從未被應用於任何一種Java虛擬機實現中。

  在一些更快的模式中,垃圾回收器並非基於引用記數技術。它們依據的思想是:對任何“活”的對象,一定能最終追溯到其存活在堆棧或靜態存儲區之中的引用。這個引用鏈條可能會穿過數個對象層次。由此,如果從堆棧和靜態存儲區開始,遍歷所有的引用,就能找到所有“活”的對象。對於發現的每個引用,必須追蹤它所引用的對象,然後是此對象包含的所有引用,如此反覆進行,直到“根源於堆棧和靜態存儲區的引用”所形成的網路全部被訪問為止。你所訪問過的對象必須都是“活”的。註意,這就解決了“交互自引用的對象組”的問題——這種現象根本不會被髮現,因此也就被自動回收了。

  在這種方式下,Java虛擬機將採用一種自適應的垃圾回收技術。至於如何處理找到的存活對象,取決於不同的Java虛擬機實現。有一種做法名為停止一複製(stop-and-copy)。顯然這意味著,先暫停程式的運行(所以它不屬於後臺回收模式),然後將所有存活的對象從當前堆複製到另一個堆,沒有被覆制的全部都是垃圾。當對象被覆制到新堆時,它們是一個挨著一個的,所以新堆保持緊湊排列,然後就可以按前述方法簡單、直接地分配新空間了。

  當把對象從一處搬到另一處時,所有指向它的那些引用都必須修正。位於堆或靜態存儲區的引用可以直接被修正,但可能還有其他指向這些對象的引用,它們在遍歷的過程中才能被找到(可以想像成有個表格,將舊地址映射至新地址)。

  對於這種所謂的“複製式回收器”而言,效率會降低,這有兩個原因。首先,得有兩個堆,然後得在這兩個分離的堆之間來回搗騰,從而得維護比實際需要多一倍的空間。某些Java虛擬機對此問題的處理方式是,按需從堆中分配幾塊較大的記憶體,複製動作發生在這些大塊記憶體之間。

  第二個問題在於複製。程式進入穩定狀態之後,可能只會產生少量垃圾,甚至沒有垃圾。儘管如此,複製式回收器仍然會將所有記憶體自一處複製到另一處,這很浪費。為了避免這種情形,一些Java虛擬機會進行檢查:要是沒有新垃圾產生,就會轉換到另一種工作模式(即“自適應”)。這種模式稱為標記一清掃(mark-and-sweep),Sun公司早期版本的Java虛擬機使用了這種技術。對一般用途而言,“標記一清掃”方式速度相當慢,但是當你知道只會產生少量垃圾甚至不會產生垃圾時,它的速度就很快了。

  “標記一清掃”所依據的思路同樣是從堆棧和靜態存儲區出發,遍歷所有的引用,進而找出所有存活的對象。每當它找到一個存活對象,就會給對象設一個標記,這個過程中不會回收任何對象。只有全部標記工作完成的時候,清理動作才會開始。在清理過程中,沒有標記的對象將被釋放,不會發生任何複製動作。所以剩下的堆空間是不連續的,垃圾回收器要是希望得到連續空間的話,就得重新整理剩下的對象。

  “停止一複製”的意思是這種垃圾回收動作不是在後臺進行的。相反,垃圾回收動作發生的同時,程式將會被暫停。在Sun公司的文檔中會發現,許多參考文獻將垃圾回收視為低優先順序的後臺進程,但事實上垃圾回收器在Sun公司早期版本的Java虛擬機中並非以這種方式實現的。當可用記憶體數量較低時,Sun版本的垃圾回收器會暫停運行程式,同樣,“標記一清掃”工作也必須在程式暫停的情況下才能進行。

  如前文所述,在這裡所討論的Java虛擬機中,記憶體分配以較大的“塊”為單位。如果對象較大,它會占用單獨的塊。嚴格來說,“停止一複製”要求在釋放舊有對象之前,必須先把所有存活對象從舊堆複製到新堆,這將導致大量記憶體複製行為。有了塊之後,垃圾回收器在回收的時候就可以往廢棄的塊里拷貝對象了。每個塊都用相應的代數(generation count)來記錄它是否還存活。通常,如果塊在某處被引用,其代數會增加。垃圾回收器將對上次回收動作之後新分配的塊進行整理。這對處理大量短命的臨時對象很有幫助。垃圾回收器會定期進行完整的清理動作——大型對象仍然不會被覆制(只是其代數會增加),內含小型對象的那些塊則被覆制並整理。Java虛擬機會進行監視,如果所有對象都很穩定,垃圾回收器的效率降低的話,就切換到 “標記一清掃”方式;同樣,Java虛擬機會跟蹤“標記一清掃”的效果,要是堆空間出現很多碎片,就會切換回“停止一複製”方式。這就是“自適應”技術,你可以給它個羅嗦的稱呼:“自適應的、分代的、停止一複製、標記一清掃”式垃圾回收器。

  Java虛擬機中有許多附加技術用以提升速度。尤其是與載入器操作有關的,被稱為“即時”(Just-In-Time,JIT)編譯器的技術。這種技術可以把程式全部或部分翻譯成本地機器碼(這本來是Java虛擬機的工作),程式運行速度因此得以提升。當需要裝載某個類(通常是在為該類創建第一個對象)時,編譯器會先找到其.class文件,然後將該類的位元組碼裝入記憶體。此時,有兩種方案可供選擇。一種是就讓即時編譯器編譯所有代碼。但這種做法有兩個缺陷:這種載入動作散落在整個程式生命周期內,累加起來要花更多時間.並且會增加可執行代碼的長度(位元組碼要比即時編譯器展開後的本地機器碼小很多),這將導致頁面調度,從而降低程式速度。另一種做法稱為惰性評估(lazy evaluation),意思是即時編譯器只在必要的時候才編譯代碼。這樣,從不會被執行的代碼也許就壓根不會被JIT所編譯。新版JDK中的Java HotSpot技術就採用了類似方法,代碼每次被執行的時候都會做一些優化,所以執行的次數越多,它的速度就越快。

5.6 成員初始化

  Java儘力保證:所有變數在使用前都能得到恰當的初始化。對於方法的局部變數,Java以編譯時錯誤的形式來貫徹這種保證。所以如果寫成:

void f()
{
    int i;
    i++;//錯誤,變數i可能沒有被初始化
}

  就會得到一條出錯消息,告訴你i可能尚未初始化。當然,編譯器也可以為i賦一個預設值,但是未初始化的局部變數更有可能是程式員的疏忽,所以採用預設值反而會掩蓋這種失誤。因此強製程序員提供一個初始值,往往能夠幫助找出程式里的缺陷。

  要是類的數據成員(即欄位)是基本類型,情況就會變得有些不同。正如在“一切都是對象”一章中所看到的,類的每個基本類型數據成員保證都會有一個初始值。下麵的程式可以驗證這類情況,並顯示它們的值:

public class InitialValues
{
    boolean t;
    char c;
    byte b;
    short s;
    int i;
    long l;
    float f;
    double d;
    InitialValues iv;

    void

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當let聲明一個變數的時候它使用的詞法作用域或者是塊作用域。塊作用域指的就是他們包含的塊以外的不能訪問。 const聲明:是let聲明有相同的作用域規則,但是它被賦值後不能再被改變。類似於java的final TypeScript 可以用 `` 來聲明多行字元串,編譯之後js文件換行的地方會加上\n ...
  • 1. 動畫 (1) Css樣式提供了運動 過渡的屬性transition 從一種情況到另一種情況叫過渡 Transition:attr time linear delay; Attr 是變化的屬性 Time 是花費的時間 Linear 變化的速度 Delay 是延遲 複習background:url ...
  • 一. for迴圈和while迴圈中的else代表什麼意思? 二. break, continue, pass, return和exit分別代表什麼意思? 三. 寫迴圈代碼時, for 和 while該如何選擇, 以及有什麼註意事項? 四. 代碼實現: 接收用戶輸入的兩個數值a和b, 使用迴圈計算出a ...
  • #關鍵:array_rand() 函數返回數組中的隨機鍵名,或者如果您規定函數返回不只一個鍵名,則返回包含隨機鍵名的數組。#思路:先使用array_rand()隨機取出所需數量鍵名,然後將這些鍵名指向的值重新組合為數組 1 /** 2 * 數組中取出隨機取出指定數量子值集 3 * @param $a ...
  • 一、php中常見的危險函數和審計要點 危險函數(功能過於強大) 參數是否外部可控,有沒有正確的過濾。 PHP獲取外界傳入參數是通過下麵幾個全局函數的形式,所以審計參數傳入經常要和下麵幾個變數打交道 PHP中危險函數有五大特性: 參數是否可控 執行任意代碼函數 1)把傳入的字元串直接當成php代碼直接 ...
  • 題意 "題目鏈接" Sol 打出暴力不難發現時間複雜度的瓶頸在於求$\sum_{i = 1}^n i^k$ 老祖宗告訴我們,這東西是個$k$次多項式,插一插就行了 cpp // luogu judger enable o2 include using namespace std; const int ...
  • 什麼是PyQuery PyQuery是一個類似於jQuery的解析網頁工具,使用lxml操作xml和html文檔,它的語法和jQuery很像。和XPATH,Beautiful Soup比起來,PyQuery更加靈活,提供增加節點的class信息,移除某個節點,提取文本信息等功能。 初始化PyQuer ...
  • 一.行為型模式 創建型模式基於對象的創建機制,隔離了對象的創建細節,使代碼能夠與要創建的對象的類型相互獨立 結構型模式用於設計對象和類的結構,使它們可以相互協作以獲得更大的結構 行為型模式主要關註對象的責任,用來處理對象之間的交互,以實現更大的功能 二.理解觀察者模式 觀察者模式是一種行為型模式,在 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...