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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...