java编程思想--5初始化与清理

一、初始化
      初始化其实就是为变量分配内存空间,并确定其初始值的过程。想了解Java中初始化的各种情况,首先要了解Java中变量的类型。根据自己的理解,将Java中的变量类型分成以下几种,虽然可能不太准确,但只是为了方便理解把握:
      (1)局部变量:内存空间被分配在堆栈上;一般是在方法中定义的变量;其初始化操作是必须的,否则编译器会报错,初始化操作有两种方式:一种是在定义的时候初始化其值,另一种是定义和初始化值分开,如下面所示:
int a = 6;
//或者
int a;
a = 6;
      (2)静态成员变量(static变量或者类变量):内存空间被分配在堆上,而且只占一份存储区域,即多个实例共享一个static变量;局部变量不能被声明为静态变量;它的初始化操作是可选的,如果不人为对其进行初始化,它将会被自动初始化,人为初始化操作是在定义时对其进行赋值;其初始化过程只在类第一次实例化或者第一次调用静态方法的时候完成的。
      (3)非静态成员变量(普通成员变量):内存空间被分配在堆上;初始化操作也是可选的,如果不人为初始化则会被自动初始化,初始化操作是在定义时对其赋值。
      (4)引用变量(对象变量):即类的实例;是通过new的方式在堆上分配内存空间的;与C++不同,Java中不允许有局部对象变量;在被new完后,类的构造器被调用,从而完成初始化过程;如果在定义类时没有定义构造器,则默认构造器会在初始化过程中被调用,构造器还可以被重载,通过参数列表的不同来区分不同的构造器。
      Java中,类在实例化后,其初始化的顺序通过下面的例子来直观说明:

package com.exercise;

import java.awt.peer.SystemTrayPeer;

class A {
    public A(int i) {
        System.out.println("A"+i+" was created!");
    }
}

class B {
    public B(int i) {
        System.out.println("Static B"+i+" was created!");
    }
}

class BaseClass {
    A a = new A(1);
    static {
        System.out.println("BaseClass's first static block was execute!");
    }
    static B b = new B(2);
    static {
        System.out.println("BaseClass's second static block was execute!");
    }
    public BaseClass() {
        System.out.println("BaseClass's constructor was called!");
    }
    A aa = new A(3);
    static B bb = new B(4);
}

class ChildClass extends BaseClass {
    A aaa = new A(5);
    static {
        System.out.println("ChildClass's first static block was execute!");
    }
    static B bbb = new B(6);
    static {
        System.out.println("ChildClass's second static block was execute!");
    }
    public ChildClass() {
        System.out.println("ChildClass's constructor was called!");
    }
    A aaaa = new A(7);
    static B bbbb = new B(8);
}

public class Test {

    /**
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("Enter the main function!");
        ChildClass childClass = new ChildClass();
        System.out.println("Leave the main function!");
    }

}

运行结果如下:

Enter the main function!
BaseClass's first static block was execute!
Static B2 was created!
BaseClass's second static block was execute!
Static B4 was created!
ChildClass's first static block was execute!
Static B6 was created!
ChildClass's second static block was execute!
Static B8 was created!
A1 was created!
A3 was created!
BaseClass's constructor was called!
A5 was created!
A7 was created!
ChildClass's constructor was called!
Leave the main function!

      根据运行结果可以看出,初始化顺序是
      1、父类静态成员变量和静态初始化块,按在代码中出现的先后顺序
      2、子类静态成员变量和静态初始化块,按在代码中出现的先后顺序
      3、父类非静态成员变量,按在代码中出现的先后顺序
      4、父类构造器
      5、子类非静态成员变量,按在代码中出现的先后顺序
      6、子类构造器
二、清理
      Java中提供了垃圾回收器用于自动回收在程序中通过new创建的内存空间,垃圾回收不能保证不用的内存空间立刻就能得到释放,如果程序对性能要求较高的话,可能要自己创建清理函数,然后在需要的时候调用。
      除了通过new方式分配内存空间,有时候可能调用了“本地方法”分配了内存空间(如调用C语言中的malloc()),针对类似的情况,Java中提供了finalize()方法来处理这类情况的内存回收,一般finalize()有用武之地的情况是,用于验证对象终结条件,从而发现潜在的缺陷。例如如下代码(摘自《Java编程思想》):

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

      void checkIn() {
           checkedOut=false;
      }

      protected void finalize() {
           if(checkedOut)
                System.ouy.println("Error:checked out"); 
                //Normally,you'll also do this:
                //super.finalize();//Call the base-class version
      }
}


public class TerminatoinCondition {
      public static void main(String[]args) {
           Book novel=new Book(true);
           //Proper cleanup:
           novel.checkIn();
           //Drop the reference,forget to clean up:
           new Bool(true);
           //Force garbage collection & finalization:
           System.gc();
      }
}

C++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java 也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。
1 用构建器自动初始化

对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在 Java 中,由于提供了名为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构建器,那么在创建对象时,Java 会自动调用那个构建器——甚至在用户毫不知觉的情况下。
接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java 里得到了应用:构建器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。
示例:
class Rock {
Rock() { // This is the constructor
System.out.println("CreatingRock");
}
}
publicclass test {
publicstaticvoid main(String[] args) {
for(inti = 0; i < 10;i++)
new Rock();
}
} ///:~
一旦创建一个对象:
new Rock();
就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同! 和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。
class Rock {
Rock(inti) {
System.out.println("Creating Rocknumber " + i);
}
}
publicclass test {
publicstaticvoid main(String[] args) {
for (inti = 0; i < 10;i++)
new Rock(i);
}
}
构建器属于一种较特殊的方法类型,因为它没有返回值。这与 void 返回值存在着明显的区别。对于void 返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。
2 方法过载

在任何程序设计语言中,一项重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的名字。方法名代表的是一种具体的行动。通过用名字描述自己的系统,可使自己的程序更易人们理解和修改。
我们用名字引用或描述所有对象与方法。若名字选得好,可使自己及其他人更易理解自己的代码。 将人类语言中存在细致差别的概念“映射”到一种程序设计语言中时,会出现一些特殊的问题。这是由于听众根本不需要对执行的行动作任何明确的区分。人类的大多数语言都具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。我们不需要独一无二的标识符——可从具体的语境中推论出含义。
大多数程序设计语言(特别是C)要求我们为每个函数都设定一个独一无二的标识符。所以绝对不能用一个名为print()的函数来显示整数,再用另一个print()显示浮点数——每个函数都要求具备唯一的名字。
在Java 里,另一项因素强迫方法名出现过载情况:构建器。由于构建器的名字由类名决定,所以只能有一个构建器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构建器,一个没有自变量(默认构建器),另一个将字串作为自变量——用于初始化对象的那个文件的名字。由于都是构建器,所以它们必须有相同的名字,亦即类名。所以为了让相同的方法名伴随不同的自变量类型使用,“方法过载”是非常关键的一项措施。同时,尽管方法过载是构建器必需的,但它亦可应用于其他任何方法,且用法非常方便。
示例如下:
importjava.util.*;
class Tree {
intheight;
Tree(){
prt("Planting aseedling");
height = 0;
}
Tree(inti) {
prt("Creating newTree that is "+i +" feet tall");
height =i;
}
void info() {
prt("Tree is " +height +" feet tall");
}
void info(Strings) {
prt(s +": Tree is " +height +" feet tall");
}
staticvoid prt(Strings) {
System.out.println(s);
}
}
publicclass test {
publicstaticvoid main(String[] args) {
for (inti = 0; i < 5;i++) {
Treet =new Tree(i);
t.info();
t.info("overloaded method");
}
// Overloadedconstructor:
new Tree();
}
} // /:~
输出如下:
Creatingnew Tree that is 0 feet tall
Treeis 0 feet tall
overloadedmethod: Tree is 0 feet tall
Creatingnew Tree that is 1 feet tall
Treeis 1 feet tall
overloadedmethod: Tree is 1 feet tall
Creatingnew Tree that is 2 feet tall
Treeis 2 feet tall
overloadedmethod: Tree is 2 feet tall
Creatingnew Tree that is 3 feet tall
Treeis 3 feet tall
overloadedmethod: Tree is 3 feet tall
Creatingnew Tree that is 4 feet tall
Treeis 4 feet tall
overloadedmethod: Tree is 4 feet tall
Plantinga seedling
Tree 既可创建成一颗种子,不含任何自变量;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了两个构建器,一个没有自变量,另一个采用现成的高度。
4.2.1 区分过载方法
若方法有同样的名字,Java 怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个过载的方法都必须采取独一无二的自变量类型列表。
即使自变量的顺序也足够我们区分两个方法(尽管我们通常不愿意采用这种方法,因为它会产生难以维护的代码)
示例如下:
publicclass test {
staticvoid print(Strings,inti) {
System.out.println(
"String:" +s +
",int: " +i);
}
staticvoid print(inti, String s) {
System.out.println(
"int:" +i +
",String: " +s);
}
publicstaticvoid main(String[] args) {
print("String first", 11);
print(99,"Int first");
}
} ///:~
3 返回值过载

为什么只有类名和方法自变量列出?为什么不根据返回值对方法加以区分?比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的:
void f() {}
int f() {}
若编译器可根据上下文(语境)明确判断出含义,比如在 int x=f()中,那么这样做完全没有问题。然而,我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我们关心的不是返回值,而是方法调用的其他效果。所以假如我们象下面这样调用方法:
f();
Java 怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分过载的方法。
4 默认构建器

默认构建器是没有自变量的。它们的作用是创建一个“空对象”。若创建一个没有构建器的类,则编译程序会帮我们自动创建一个默认构建器。
如下:
class Bird {
inti;
}
publicclass test {
publicstaticvoid main(String[] args) {
Bird nc =new Bird();// default!
}
} ///:~
new Bird(); 它的作用是新建一个对象,并调用默认构建器——即使尚未明确定义一个象这样的构建器。若没有它,就没有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构建器(无论是否有自变量),编译程序都不会帮我们自动合成一个
5 this 关键字

如果有两个同类型的对象,分别叫作a 和b,那么您也许不知道如何为这两个对象同时调用一个f()方法:
class Banana { void f(int i) { /* ... */ }}
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);
若只有一个名叫f()的方法,它怎样才能知道自己是为 a 还是为b 调用的呢?
为了能用简便的、面向对象的语法来书写代码——亦即“将消息发给对象”,编译器为我们完成了一些幕后工作。其中的秘密就是第一个自变量传递给方法f(),而且那个自变量是准备操作的那个对象的句柄。所以前述的两个方法调用就变成了下面这样的形式:
Banana.f(a,1);
Banana.f(b,2);
这是内部的表达形式,我们并不能这样书写表达式,并试图让编译器接受它。但是,通过它可理解幕后到底发生了什么事情。
假定我们在一个方法的内部,并希望获得当前对象的句柄。由于那个句柄是由编译器“秘密”传递的,所以没有标识符可用。然而,针对这一目的有个专用的关键字:this。
this 关键字(注意只能在方法内部使用)可为已调用了其方法的那个对象生成相应的句柄。可象对待其他任何对象句柄一样对待这个句柄。但要注意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用this。只需简单地调用那个方法即可。当前的this 句柄会自动应用于其他方法。
this 关键字只能用于那些特殊的类——需明确使用当前对象的句柄。例如,假若您希望将句柄返回给当前对象,那么它经常在return 语句中使用。
示例如下:
publicclass test {
privateinti = 0;
test increment() {
i++;
returnthis;
}
void print() {
System.out.println("i = " + i);
}
publicstaticvoid main(String[] args) {
test x =new test();
x.increment().increment().increment().print();
}
} ///:~
由于increment()通过this 关键字返回当前对象的句柄,所以可以方便地对同一个对象执行多项操作。
5.1 在构建器里调用构建器(this)

若为一个类写了多个构建器,那么经常都需要在一个构建器里调用另一个构建器,以避免写重复的代码。可用this 关键字做到这一点。
通常,当我们说this 的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个句柄。在一个构建器中,若为其赋予一个自变量列表,那么 this 关键字会具有不同的含义:它会对与那个自变量列表相符的构建器进行明确的调用。这样一来,我们就可通过一条直接的途径来调用其他构建器。
示例如下:
publicclass Flower {
privateintpetalCount = 0;
private Strings =new String("null");
Flower(intpetals ) {
petalCount =petals;
System.out.println("Constructor w/int arg only, petalCount= "
+petalCount);
}
Flower(Stringss) {
System.out.println("Constructor w/ String arg only, s=" +ss);
s =ss;
}
Flower(Strings,intpetals) {
this(petals);
// ! this(s); //Can&#39;t call two!
this.s = s;// Another use of "this"
System.out.println("String &int args");
}
Flower(){
this("hi", 47);
System.out.println("defaultconstructor (no args)");
}
void print() {
// ! this(11); // Notinside non-constructor!
System.out.println("petalCount =" + petalCount +" s = " +s);
}
publicstaticvoid main(String[] args) {
Flowerx =new Flower();
x.print();
}
} // /:~
输出如下:
Constructorw/ int arg only, petalCount= 47
String& int args
defaultconstructor (no args)
petalCount= 47 s = hi
尽管可用this 调用一个构建器,但不可调用两个。除此以外,构建器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。这个例子也向大家展示了this 的另一项用途。由于自变量s 的名字以及成员数据s 的名字是相同的,所以会出现混淆。为解决这个问题,可用 this.s来引用成员数据。经常都会在 Java 代码里看到这种形式的应用。在print()中,我们发现编译器不让我们从除了一个构建器之外的其他任何方法内部调用一个构建器。
5.2 static 的含义

理解了this 关键字后,我们可更完整地理解static(静态)方法的含义。它意味着一个特定的方法没有this。我们不可从一个 static方法内部发出对非 static方法的调用,尽管反过来说是可以的。
而且在没有任何对象的前提下,我们可针对类本身发出对一个 static方法的调用。事实上,那正是 static方法最基本的意义。它就好象我们创建一个全局函数的等价物(在C 语言中)。除了全局函数不允许在Java中使用以外,若将一个 static方法置入一个类的内部,它就可以访问其他static 方法以及static 字段。
有些人抱怨 static方法并不是“面向对象”的,因为它们具有全局函数的某些特点;利用static方法,我们不必向对象发送一条消息,因为不存在this。这可能是一个清楚的自变量,若您发现自己使用了大量静态方法,就应重新思考自己的策略。然而,static 的概念是非常实用的,许多时候都需要用到它。所以至于它们是否真的“面向对象”,应该留给理论家去讨论。事实上,即使Smalltalk 在自己的“类方法”里也有类似于static的东西。
6 清除:收尾和垃圾收集

程序员都知道“初始化”的重要性,但通常忘记清除的重要性。
但是对于库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java可用垃圾收集器回收由不再使用的对象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没有使用new。
垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,可为我们的类定义它。在理想情况下,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作。
但也是一个潜在的编程陷阱,因为有些程序员(特别是在C++开发背景的)刚开始可能会错误认为它就是在C++中为“破坏器”(Destructor)使用的finalize()——破坏(清除)一个对象的时候,肯定会调用这个函数。有必要区分一下C++和Java 的区别,因为C++的对象肯定会被清除(排开编程错误的因素),而Java 对象并非肯定能作为垃圾被“收集”去。或者换句话说: 垃圾收集并不等于“破坏”!
Java 并未提供“破坏器”或者类似的概念,所以必须创建一个原始的方法,用它来进行这种清除。例如,假设在对象创建过程中,它会将自己描绘到屏幕上。如果不从屏幕明确删除它的图像,那么它可能永远都不会被清除。若在finalize()里置入某种删除机制,那么假设对象被当作垃圾收掉了,图像首先会将自身从屏幕上移去。但若未被收掉,图像就会保留下来。所以要记住:我们的对象可能不会当作垃圾被收掉!
有时可能发现一个对象的存储空间永远都不会释放,因为自己的程序永远都接近于用光空间的临界点。若程序执行结束,而且垃圾收集器一直都没有释放我们创建的任何对象的存储空间,则随着程序的退出,那些资源会返回给操作系统。这是一件好事情,因为垃圾收集本身也要消耗一些开销。如永远都不用它,那么永远也不用支出这部分开销。
6.1 finalize() 用途何在

垃圾收集只跟内存有关!
垃圾收集器存在的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾收集有关的任何活动来说,其中最值得注意的是finalize()方法,它们也必须同内存以及它的回收有关。
但这是否意味着假如对象包含了其他对象,finalize()就应该明确释放那些对象呢?答案是否定的——垃圾收集器会负责释放所有对象占据的内存,无论这些对象是如何创建的。它将对finalize()的需求限制到特殊的情况。在这种情况下,我们的对象可采用与创建对象时不同的方法分配一些存储空间。
之所以要使用finalize(),看起来似乎是由于有时需要采取与Java的普通方法不同的一种方法,通过分配内存来做一些具有C 风格的事情。这主要可以通过“固有方法”来进行,它是从Java 里调用非Java 方法的一种方式。C和C++是目前唯一获得固有方法支持的语言。
但由于它们能调用通过其他语言编写的子程序,所以能够有效地调用任何东西。在非Java 代码内部,也许能调用C 的malloc()系列函数,用它分配存储空间。而且除非调用了free(),否则存储空间不会得到释放,从而造成内存“漏洞”的出现。当然,free()是一个C 和C++函数,所以我们需要在finalize()内部的一个固有方法中调用它。
6.2 必须执行清除

为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这听起来似乎很容易做到,但却与 C++“破坏器”的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对象都“应该”破坏。若将C++对象创建成一个本地对象,比如在堆栈中创建(在 Java 中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的、创建这个对象的作用域的末尾进行。若对象是用new创建的(类似于 Java),那么当程序员调用 C++的delete 命令时(Java 没有这个命令),就会调用相应的破坏器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”,另外还包括对象的其他部分永远不会得到清除。
相反,Java 不允许我们创建本地(局部)对象——无论如何都要使用new。但在Java 中,没有“delete”命令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说正是由于存在垃圾收集机制,所以 Java 没有破坏器。然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调用finalize(),所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java 中的一个方法。它等价于C++的破坏器,只是没后者方便。
finalize()最有用处的地方之一是观察垃圾收集的过程。
示例:
class Chair {
staticbooleangcrun = false;
staticbooleanf = false;
staticintcreated = 0;
staticintfinalized = 0;
inti;
Chair(){
i = ++created;
if (created == 47)
System.out.println("Created47");
}
protectedvoid finalize() {
if (!gcrun) {
gcrun =true;
System.out.println("Beginning tofinalize after " + created
+"Chairs have been created");
}
if (i == 47) {
System.out.println("FinalizingChair #47, "
+"Settingflag to stop Chair creation");
f =true;
}
finalized++;
if (finalized >= created)
System.out.println("All " + finalized +" finalized");
}
}
publicclass Flower {
publicstaticvoid main(String[] args) {
if (args.length == 0) {
System.err.println("Usage: \n" + "java Garbage before\n or:\n"
+"javaGarbage after");
return;
}
while (!Chair.f) {
new Chair();
new String("To take up space");
}
System.out.println("After allChairs have been created:\n"
+"totalcreated = "+ Chair.created + ", total finalized = "
+Chair.finalized);
if (args[0].equals("before")) {
System.out.println("gc():");
System.gc();
System.out.println("runFinalization():");
System.runFinalization();
}
System.out.println("bye!");
if (args[0].equals("after"))
System.runFinalizersOnExit(true);
}
} // /:~
输出如下:

上面这个程序创建了许多Chair 对象,而且在垃圾收集器开始运行后的某些时候,程序会停止创建Chair。
由于垃圾收集器可能在任何时间运行,所以我们不能准确知道它在何时启动。因此,程序用一个名为gcrun的标记来指出垃圾收集器是否已经开始运行。利用第二个标记 f,Chair 可告诉 main()它应停止对象的生成。这两个标记都是在 finalize()内部设置的,它调用于垃圾收集期间。
另两个static 变量——created 以及finalized——分别用于跟踪已创建的对象数量以及垃圾收集器已进行完收尾工作的对象数量。最后,每个Chair 都有它自己的(非static)int i,所以能跟踪了解它具体的编号是多少。编号为47 的Chair 进行完收尾工作后,标记会设为true,最终结束Chair 对象的创建过程。
运行这个程序的时候,提供了一个命令行自变量“before”或者“after”。其中,“before”自变量会调用System.gc()方法(强制执行垃圾收集器),同时还会调用System.runFinalization()方法,以便进行收尾。
调用的runFinalizersOnExit()方法却只有Java 1.1 及后续版本提供了对它的支持。注意可在程序执行的任何时候调用这个方法,而且收尾程序的执行与垃圾收集器是否运行是无关的。
若使用一个不是“before”或“after”的自变量(如“none”),那么两个收尾工
作都不会进行,而且我们会得到象下面这样的输出
totalcreated = 322, total finalized = 187
为强制进行收尾工作,可先调用System.gc(),再调用System.runFinalization()。这样可清除到目前为止没有使用的所有对象。这样做一个稍显奇怪的地方是在调用runFinalization()之前调用gc(),这看起来似乎与 Sun公司的文档说明有些抵触,它宣称首先运行收尾模块,再释放存储空间。然而,若在这里首先调用runFinalization(),再调用gc(),收尾模块根本不会执行。
7 成员初始化

Java 尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变量,这一保证就通过编译期的出错提示表现出来。
初始化示例:
class Measurement {
booleant;
charc;
byteb;
shorts;
inti;
longl;
floatf;
doubled;
void print() {
System.out.println(
"Data typeInital value\n" +
"boolean" +t +"\n" +
"char" +c +"\n" +
"byte" +b +"\n" +
"short" +s +"\n" +
"int" +i +"\n" +
"long" +l +"\n" +
"float" +f +"\n" +
"double" +d);
}
}
publicclass Flower {
publicstaticvoid main(String[] args) {
Measurementd =new Measurement();
d.print();
/* In this case you could also say:
new Measurement().print();
*/
}
}///:~
输出如下:
Data type Inital value
boolean false
char
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
其中,Char 值为空(NULL),没有数据打印出来。
在一个类的内部定义一个对象句柄时,如果不将其初始化成新对象,那个句柄就会获得一个空值。
7.1 规定初始化

如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部定义变量的同时也为其赋值(注意在C++里不能这样做,尽管C++的新手们总“想”这样做)
7.2 构建器初始化

7.2.1初始化顺序

可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建器进入之前就会发生。
示例如下:
class Tag {
Tag(intmarker) {
System.out.println("Tag(" + marker +")");
}
}
class Card {
Tagt1 =new Tag(1);// Before constructor
Card(){
// Indicate we&#39;re inthe constructor:
System.out.println("Card()");
t3 =new Tag(33);// Re-initialize t3
}
Tagt2 =new Tag(2);// After constructor
void f() {
System.out.println("f()");
}
Tagt3 =new Tag(3);// At end
}
publicclass Flower {
publicstaticvoid main(String[] args) {
Cardt =new Card();
t.f();// Shows that construction is done
}
}
输出如下:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
t3句柄会被初始化两次,一次在构建器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个过载的构建器,它没有初始化 t3;同时在t3 的定义里并没有规定“默认”的初始化方式,那么会产生什么后果呢?
7.2.2静态数据的初始化

若数据是静态的(static),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的句柄,那么除非新建一个对象,并将句柄同它连接起来,否则就会得到一个空值(NULL)。
如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于static 值只有一个存储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。
示例如下:
class Bowl {
Bowl(intmarker) {
System.out.println("Bowl(" + marker +")");
}
void f(intmarker) {
System.out.println("f(" + marker +")");
}
}
class Table {
static Bowlb1 =new Bowl(1);
Table(){
System.out.println("Table()");
b2.f(1);
}
void f2(intmarker) {
System.out.println("f2(" + marker +")");
}
static Bowlb2 =new Bowl(2);
}
class Cupboard {
Bowlb3 =new Bowl(3);
static Bowlb4 =new Bowl(4);
Cupboard(){
System.out.println("Cupboard()");
b4.f(2);
}
void f3(intmarker) {
System.out.println("f3(" + marker +")");
}
static Bowlb5 =new Bowl(5);
}
publicclass Flower {
publicstaticvoid main(String[] args) {
System.out.println("Creating newCupboard() in main");
new Cupboard();
System.out.println("Creating newCupboard() in main");
new Cupboard();
t2.f2(1);
t3.f3(1);
}
static Tablet2 =new Table();
static Cupboardt3 =new Cupboard();
} // /:~
输出如下:
Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creatingnew Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creatingnew Cupboard() in main
Bowl(3)
Cupboard()
f(2)
f2(1)
f3(1)
小伙伴可以自己观察这个执行的先后顺序。
static初始化只有在必要的时候才会进行。如果不创建一个Table 对象,而且永远都不引用Table.b1或Table.b2,那么 static Bowl b1 和b2 永远都不会创建。然而,只有在创建了第一个Table 对象之后(或者发生了第一次static访问),它们才会创建。在那以后,static 对象不会重新初始化。
初始化的顺序是首先static(如果它们尚未由前一次对象创建过程初始化),接着是非static 对象。
创建过程。考虑一个名为Dog的类:
(1) 类型为 Dog的一个对象首次创建时,或者Dog类的static方法/static 字段首次访问时,Java 解释器必须找到Dog.class(在事先设好的类路径里搜索)。
(2) 找到Dog.class 后(它会创建一个 Class对象,这将在后面学到),它的所有static初始化模块都会运行。因此,static初始化仅发生一次——在Class 对象首次载入的时候。
(3) 创建一个new Dog()时,Dog 对象的构建进程首先会在内存堆(Heap)里为一个 Dog对象分配足够多的存储空间。
(4) 这种存储空间会清为零,将Dog中的所有基本类型设为它们的默认值(零用于数字,以及 boolean和char 的等价设定)。
(5) 进行字段定义时发生的所有初始化都会执行。
(6) 执行构建器。这实际可能要求进行相当多的操作,特别是在涉及继承的时候。
7.2.3明确进行的静态初始化

Java 允许我们将其他static初始化工作划分到类内一个特殊的“static 构建从句”(有时也叫作“静态块”)里。
尽管看起来象个方法,但它实际只是一个static 关键字,后面跟随一个方法主体。与其他 static初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个static 成员时(即便从未生成过那个类的对象)。
举例如下:
class Cup {
Cup(intmarker) {
System.out.println("Cup(" + marker +")");
}
void f(intmarker) {
System.out.println("f(" + marker +")");
}
}
class Cups {
static Cupc1;
static Cupc2;
static {
c1 =new Cup(1);
c2 =new Cup(2);
}
Cups(){
System.out.println("Cups()");
}
}
publicclass Flower {
publicstaticvoid main(String[] args) {
System.out.println("Insidemain()");
Cups.c1.f(99);// (1)
}
static Cupsx =new Cups();// (2)
static Cupsy =new Cups();// (2)
} // /:~
输出如下:
Cup(1)
Cup(2)
Cups()
Cups()
Insidemain()
f(99)
在标记为(1)的行内访问 static 对象c1 的时候,或在行(1)标记为注释,同时(2)行不标记成注释的时候,用于Cups 的static初始化模块就会运行。若(1)和(2)都被标记成注释,则用于Cups 的static 初始化进程永远不会发生。
7.2.4非静态实例的初始化

针对每个对象的非静态变量的初始化,Java 1.1 提供了一种类似的语法格式。
示例如下:
class Mug {
Mug(intmarker) {
System.out.println("Mug(" + marker +")");
}
void f(intmarker) {
System.out.println("f(" + marker +")");
}
}
publicclass Flower {
Mug c1;
Mug c2;
{
c1 =new Mug(1);
c2 =new Mug(2);
System.out.println("c1 & c2initialized");
}
Flower() {
System.out.println("Mugs()");
}
publicstaticvoid main(String[] args) {
System.out.println("Insidemain()");
Flower x =new Flower();
}
} ///:~
输出如下:
Insidemain()
Mug(1)
Mug(2)
c1& c2 initialized
Mugs()
它看起来与静态初始化从句极其相似,只是static 关键字从里面消失了。为支持对“匿名内部类”的初始化,必须采用这一语法格式。
7.3 数组初始化

在C 中初始化数组极易出错,而且相当麻烦。C++通过“集合初始化”使其更安全。Java 则没有象C++那样的“集合”概念,因为Java中的所有东西都是对象。但它确实有自己的数组,通过数组初始化来提供支持。
数组代表一系列对象或者基本数据类型,所有相同的类型都封装到一起——采用一个统一的标识符名称。数组的定义和使用是通过方括号索引运算符进行的([])。为定义一个数组,只需在类型名后简单地跟随一对空方括号即可:
int[] al;
也可以将方括号置于标识符后面,获得完全一致的结果:
int al[];
这种格式与 C和C++程序员习惯的格式是一致的。然而,最“通顺”的也许还是前一种语法,因为它指出类型是“一个 int 数组”。
编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“句柄”的问题上。此时,我们拥有的一切就是指向数组的一个句柄,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。存储空间的分配(等价于使用new)将由编译器在这种情况下进行。例如:
int[] a1 = { 1,2, 3, 4, 5 };
那么为什么还要定义一个没有数组的数组句柄呢?
int[] a2;
事实上在Java 中,可将一个数组分配给另一个,所以能使用下述语句:
a2 = a1;
我们真正准备做的是复制一个句柄.
示例如下:
publicclass test {
publicstaticvoid main(String[] args) {
int[]a1 = { 1, 2, 3, 4, 5 };
int[]a2;
a2 =a1;
for(inti = 0; i
a2[i]++;
for(inti = 0; i
prt("a1[" +i +"] = " +a1[i]);
}
staticvoid prt(Strings) {
System.out.println(s);
}
} ///:~
输出如下:
a1[0]= 2
a1[1]= 3
a1[2]= 4
a1[3]= 5
a1[4] = 6
a1 获得了一个初始值,而a2 没有;a2将在以后赋值——这种情况下是赋给另一个数组。
所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是length。与C和C++类似,由于Java 数组从元素 0 开始计数,所以能索引的最大元素编号是“length-1”。如超出边界,C 和C++会“默默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。
然而,Java 可保留我们这受这一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“违例”,就是Exception)。当然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。
但考虑到因特网访问的安全,以及程序员的编程效率,Java 设计人员还是应该把它看作是值得的。 程序编写期间,如果不知道在自己的数组里需要多少元素,那么又该怎么办呢?此时,只需简单地用new在数组里创建元素。在这里,即使准备创建的是一个基本数据类型的数组,new也能正常地工作(new不会创建非数组的基本类型)
示例如下:
import java.util.*;
publicclass test {
static Randomrand =new Random();
staticint pRand(intmod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
publicstaticvoid main(String[] args) {
int[]a;
a =newint[pRand(20)];
prt("length of a = " +a.length);
for(inti = 0; i
prt("a["+i +"] = " +a[i]);
}
staticvoid prt(Strings) {
System.out.println(s);
}
} ///:~
输出结果:
lengthof a = 12
a[0]= 0
a[1]= 0
a[2]= 0
a[3]= 0
a[4]= 0
a[5]= 0
a[6]= 0
a[7]= 0
a[8]= 0
a[9]= 0
a[10]= 0
a[11]= 0
由于数组的大小是随机决定的(使用早先定义的pRand()方法),所以非常明显,数组的创建实际是在运行期间进行的。除此以外,从这个程序的输出中,大家可看到基本数据类型的数组元素会自动初始化成“空”值(对于数值,空值就是零;对于 char,它是null;而对于boolean,它却是false)。
当然,数组可能已在相同的语句中定义和初始化了,如下所示:
int[] a = new int[pRand(20)];
若操作的是一个非基本类型对象的数组,那么无论如何都要使用new。在这里,我们会再一次遇到句柄问题,因为我们创建的是一个句柄数组。请大家观察封装器类型 Integer,它是一个类,而非基本数据类型。
再来看个例子,由于所有类最终都是从通用的根类Object 中继承的,所以能创建一个方法,令其获取一个 Object数组:
class A {
inti;
}
publicclass test {
staticvoid f(Object[]x) {
for (inti = 0; i
System.out.println(x[i]);
}
publicstaticvoid main(String[] args) {
f(new Object[] {new Integer(47),new test(),new Float(3.14),
new Double(11.11) });
f(new Object[] {"one","two","three" });
f(new Object[] {new A(),new A(),new A() });
}
} // /:~
输出如下:
47
test@5e374549
3.14
11.11
one
two
three
A@e11d0a85
A@82a4d9fb
A@e4d67d99
我们对这些未知的对象并不能采取太多的操作,而且这个程序利用自动String转换对每个 Object 做一些有用的事情。
7.4 多维数组

在Java 里可以方便地创建多维数组,示例如下:
import java.util.*;
publicclass test {
static Randomrand =new Random();
staticint pRand(intmod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
publicstaticvoid main(String[] args) {
int[][]a1 = { { 1, 2, 3, }, { 4, 5, 6, }, };
for (inti = 0; i
for (intj = 0; j
prt("a1[" +i +"][" +j +"] = " +a1[i][j]);
// 3-D array withfixed length:
int[][][]a2 =newint[2][2][4];
for (inti = 0; i
for (intj = 0; j
for (intk = 0; k
prt("a2[" +i +"][" +j +"][" +k +"] = " +a2[i][j][k]);
// 3-D array withvaried-length vectors:
int[][][]a3 =newint[pRand(7)][][];
for (inti = 0; i
a3[i] =newint[pRand(5)][];
for (intj = 0; j
a3[i][j] = newint[pRand(5)];
}
for (inti = 0; i
for (intj = 0; j
for (intk = 0; k
prt("a3[" +i +"][" +j +"][" +k +"] = " +a3[i][j][k]);
// Array ofnon-primitive objects:
Integer[][]a4 = { {new Integer(1),new Integer(2) },
{new Integer(3),new Integer(4) },
{new Integer(5),new Integer(6) }, };
for (inti = 0; i
for (intj = 0; j
prt("a4[" +i +"][" +j +"] = " +a4[i][j]);
Integer[][]a5;
a5 =new Integer[3][];
for (inti = 0; i
a5[i] =new Integer[3];
for (intj = 0; j
a5[i][j] = new Integer(i *j);
}
for (inti = 0; i
for (intj = 0; j
prt("a5[" +i +"][" +j +"] = " +a5[i][j]);
}
staticvoid prt(Strings) {
System.out.println(s);
}
} // /:~
输出如下:
a1[0][0]= 1
a1[0][1]= 2
a1[0][2]= 3
a1[1][0]= 4
a1[1][1]= 5
a1[1][2]= 6
a2[0][0][0]= 0
a2[0][0][1]= 0
a2[0][0][2]= 0
a2[0][0][3]= 0
a2[0][1][0]= 0
a2[0][1][1]= 0
a2[0][1][2]= 0
a2[0][1][3]= 0
a2[1][0][0]= 0
a2[1][0][1]= 0
a2[1][0][2]= 0
a2[1][0][3]= 0
a2[1][1][0]= 0
a2[1][1][1]= 0
a2[1][1][2]= 0
a2[1][1][3]= 0
a3[0][0][0]= 0
a3[0][0][1]= 0
a3[0][0][2]= 0
a3[0][1][0]= 0
a3[0][1][1]= 0
a3[0][1][2]= 0
a3[0][1][3]= 0
a3[0][2][0]= 0
a3[0][3][0]= 0
a3[0][3][1]= 0
a3[0][3][2]= 0
a3[1][0][0]= 0
a3[1][0][1]= 0
a3[1][0][2]= 0
a3[1][0][3]= 0
a3[1][0][4]= 0
a3[1][1][0]= 0
a3[1][1][1]= 0
a3[1][2][0]= 0
a3[1][2][1]= 0
a3[1][2][2]= 0
a3[1][2][3]= 0
a3[2][0][0]= 0
a3[2][0][1]= 0
a3[2][0][2]= 0
a3[2][1][0]= 0
a3[2][1][1]= 0
a3[2][1][2]= 0
a3[2][1][3]= 0
a3[2][1][4]= 0
a3[2][2][0]= 0
a3[2][2][1]= 0
a3[2][2][2]= 0
a3[2][2][3]= 0
a4[0][0]= 1
a4[0][1]= 2
a4[1][0]= 3
a4[1][1]= 4
a4[2][0]= 5
a4[2][1]= 6
a5[0][0]= 0
a5[0][1]= 0
a5[0][2]= 0
a5[1][0]= 0
a5[1][1]= 1
a5[1][2]= 2
a5[2][0]= 0
a5[2][1]= 2
a5[2][2]= 4
用于打印的代码里使用了length,所以它不必依赖固定的数组大小。
第一个例子展示了基本数据类型的一个多维数组。我们可用花括号定出数组内每个矢量的边界:
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
每个方括号对都将我们移至数组的下一级。
第二个例子展示了用new分配的一个三维数组。在这里,整个数组都是立即分配的:
int[][][] a2 = new int[2][2][4];
第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度:
int[][][] a3 = new int[pRand(7)][][];
对于第一个 new创建的数组,它的第一个元素的长度是随机的,其他元素的长度则没有定义。for循环内的第二个new 则会填写元素,但保持第三个索引的未定状态——直到碰到第三个new。
根据输出结果,看到:假若没有明确指定初始化值,数组值就会自动初始化成零。
可用类似的表式处理非基本类型对象的数组。这从第四个例子可以看出,它向我们演示了用花括号收集多个new表达式的能力:
Integer[][] a4 = {
{ new Integer(1), new Integer(2)},
{ new Integer(3), new Integer(4)},
{ new Integer(5), new Integer(6)},
};
第五个例子展示了如何逐渐构建非基本类型的对象数组:
Integer[][] a5;
a5 = new Integer[3][];
for(int i = 0; i < a5.length; i++) {
a5[i] = new Integer[3];
for(int j = 0; j < a5[i].length; j++)
a5[i][j] = new Integer(i*j);
}
i*j只是在 Integer里置了一个的值。
8 总结

作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与 C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。
在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new 创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以 Java 中等价的清除方法并不是经常都需要用到的。如果不需要类似于构建器的行为,Java 的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java 解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java 不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。
由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。

构造器保证初始化

构造器采用与类相同的名称。

默认构造器(default constructor):不接受任何参数的构造器,也叫无参构造器。

构造器也能带有形式参数,就能在初始化对象时提供实际参数。

class Foo{
Foo(){
    System.out.println("Foo's no-arg Constructor Foo()");
}
Foo(int i){
     System.out.println("Foo's arg Constructor Foo ("+i+")");
}
}
public class ConstructorDemo {
  public static void main(String[] args) {
    new Foo();
    new Foo(1);
  }
}
方法重载(Method Overloading)

定义:方法名不同而形式参数不同,适用于构造器和其它方法。

区分重载方法:每个重载的方法都必须有一个独一无二的参数类型列表。

涉及基本类型的重载:基本类型从一个较小的类型自动提升到一个较大的类型。

以返回值区分重载方法:行不通,如下所示:

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

f();
Java无法判断该调用哪个f()方法。

默认构造器

默认构造器没有形式参数,作用是创建一个默认对象。若类中没有构造器,则编译器会自动创建一个默认构造器。

没有提供构造器->编译器认为需要一个构造器,我给你制造一个。

已经有了构造器->编译器认为已写了构造器,你知道你在做什么,你是刻意省略默认构造器的。

this关键字

class Banana{
  void peel(int i){
     System.out.println(this.toString()+" "+i);
  }
}
public class BananaPeel {
    public static void main(String[] args) {
      Banana a = new Banana(),
            b = new Banana();
      System.out.println("a:"+a.toString()+"\nb:"+b.toString());
      a.peel(1);
      b.peel(2);
  }
}
结果输出:

a:Banana@610455d6
b:Banana@511d50c0
Banana@610455d6 1
Banana@511d50c0 2
同一个类的两个对象,调用类的同一方法,是如何知道是被哪个对象调用的呢?

为了能用简便,面向对象的语法来编写代码——即发送消息给对象,编译器做了一些幕后工作,它暗自把“所操作对象的引用”作为
第一个参数传递给peel()。所以a,b调用的两个方法就变成了这样:

Banana.peel(a,1);
Banana.peel(b,2);
从结果输出也能看出来编译器将所操作对象的引用传递给了peel()方法。

this关键字用于在方法内部获得对当前对象的引用。表示对"调用方法的那个对象"的引用。

this的用法和其他对象引用没有不同。

注意:在方法内部调用同一个类的另一个方法不必使用this,直接调用即可。

  public class ThisDemo {
   int i = 0;
   ThisDemo increment(){
       i++;
       //返回对当前对象的引用
       return this;
   }
   void print(){
       System.out.println("i="+i);
   }

public static void main(String[] args) {
    ThisDemo thisDemo = new ThisDemo();
    thisDemo.increment().increment().increment().increment().print();
}
构造器中调用构造器

一个类中有多个构造器,为避免重复代码,可能在一个构造器中调用另一个构造器。使用this关键字调用其他构造器。

除构造器之外,编译器禁止在其他任何方法中调用构造器。

static的含义

static(静态)方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来可以。

static方法的主要用途:可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。(很像全局方法,但是在Java中禁止使用全局方法,但在类中置入static方法就可以访问其他static方法和static域)

由于不存在this,所以静态方法不是通过"向对象发送消息"的方式来完成的。

清理:终结处理和垃圾回收

Java有垃圾回收器负责回收无用对象占据的内存资源。但有特殊情况:假定对象(并非使用new)获得一块特殊的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块特殊的内存。

针对上述特殊情况,Java允许在类中定义一个名为finalize()方法,在下一次垃圾回收动作发生时,才会真正回收对象占用的内存

对象可能不被垃圾回收
垃圾回收并不等于"析构"
在不再需要某个对象之前,如果必须执行某些动作,必须得自己去做。Java中并未提供"析构函数"或相似的概念。自己动手创建一个执行清理工作的普通方法来做类似的清理工作。

finalize()的用途

垃圾回收只与内存有关
无论是垃圾回收还是终结,都不保证一定会发生。如果Java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去
执行垃圾回收以恢复内存的。

垃圾回收器如何工作

当垃圾回收器工作时,一面回收空间,一面使堆中的对象紧凑排列。堆指针就能很容易移动到更靠近传送带的开始处,尽量避免了
页面错误。

Java编程思想之五-初始化与清理
 
初始化和清理的作用:为了提高程序的安全性,消除资源占用的浪费,Java当中采用了构造器来初始化,提供了“垃圾回收器”是内存资源自动回收。
Java当中,提供了构造器来进行初始化操作,因此就没有必要在每个类都要定义一个initialize () 方法来进行初始化操作。
Java类的构造器名称必须与类名相同,它是一种特殊类型的方法,因为它没有返回值(new表达式返回的是新建对象的引用,但构造器本身并没有任何返回值),而且没有参数的构造器是默认构造器,从概念上讲,“初始化”和“创建”是彼此独立的,但是在Java当中,“初始化”和“创建”捆绑在一起,两者不分离。
方法的重载是指方法名相同,参数不同,Java通过参数类型列表来确定执行的是哪一个方法,传输过程中,如果传入的数据类型小于方法中声明的形式参数类型,实际数据类型就会被提升,另外,根据方法的返回值来区分重载方法行不通。
Java当中,this关键字只能在方法内部使用,表示对当前对象的引用,如果在方法内部调用同类中另一个方法,不必使用this,直接调用即可。
Java静态类,static方法内部不能调用非静态方法,static方法就是没有this的方法。
Java当中的垃圾回收器只知道释放那些经由new分配的内存,对于并非使用new获得的那些内存,Java允许在类中定义一个名为finalize()的方法,它的工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,会首先调用finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存,如果程序执行结束,垃圾回收器一直都没有释放所创建的任何对象的存储空间,程序退出时,这些资源也会全部交还给操作系统。
对象可能不被垃圾回收,垃圾回收不等于析构,垃圾回收只与内存有关;无论是“垃圾回收”还是“终结”,都不能保证一定会发生,如果Java虚拟机并没有面临内存耗尽的情况下,它不会浪费时间去执行垃圾回收去释放内存;System.gc()用于强制进行终结动作。
Java垃圾回收采用的是自适应工作模式,“停止-复制”要求在释放旧有对象之前,必须把存活的对象把旧堆复制到新堆,将会导致大量的内存复制行为,垃圾回收引入了块,有了块之后,垃圾回收器在回收的时候就可以望废弃的块中复制对象了,每个块都有相应的代数来记录它是否存活,块在某处被引用,代数也会增加,垃圾回收器将对上次回收动作之后新分配的块进行整理,垃圾回收器会定期进行完整的清理动作-大型对象不会被复制(只是代数会增加),内含小型对象的那些块则被复制并整理,Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式,同样,Java虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式,这就是“自适应”技术。
在同一个类中无论局部变量放在任何位置,它们都会在调用构造器或其他方法之前得到初始化。
无论创建多少个对象,静态数据都只占用一份存储区域,初始化的顺序是先静态对象,而后是“非静态”对象,构造器实际上也是静态的。
在类的内部,变量定义的先后顺序决定了初始化的顺序,即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
Static关键字不能应用于局部变量,它只能作用于域,如果一个域是静态的基本类型域,且没有初始化,那么,它默认是基本数据类型的初值,如果是对象引用,它默认的初值就为null。
所有数组都有一个固有成员length,Java数组默认从0开始,以length-1结束。
Arrays.toString()方法属于java.util标准类库,它将产生一维数组的可打印版本。
枚举类型enum关键字,枚举类型的实例是常量,按照命名惯例它们都用大写字母表示。

初始化 构造器保证初始化
构造器采用与类相同的名称。
默认构造器(default constructor):不接受任何参数的构造器,也叫无参构造器。
构造器也能带有形式参数,就能在初始化对象时提供实际参数。
class Foo{  Foo(){    System.out.println("Foo's no-arg Constructor Foo()");  }  Foo(int i){      System.out.println("Foo's arg Constructor Foo ("+i+")");  } } public class ConstructorDemo {  public static void main(String[] args) {    new Foo();    new Foo(1);  } } 方法重载(Method Overloading)
定义:方法名不同而形式参数不同,适用于构造器和其它方法。
区分重载方法:每个重载的方法都必须有一个独一无二的参数类型列表。
涉及基本类型的重载:基本类型从一个较小的类型自动提升到一个较大的类型。
以返回值区分重载方法:行不通,如下所示:
void f(){}  int f(){      return 1;  }  f();
Java无法判断该调用哪个f()方法。 默认构造器
默认构造器没有形式参数,作用是创建一个默认对象。若类中没有构造器,则编译器会自动创建一个默认构造器。
没有提供构造器->编译器认为需要一个构造器,我给你制造一个。
已经有了构造器->编译器认为已写了构造器,你知道你在做什么,你是刻意省略默认构造器的。 this关键字
class Banana{  void peel(int i){      System.out.println(this.toString()+" "+i);  } } public class BananaPeel {    public static void main(String[] args) {      Banana a = new Banana(),            b = new Banana();      System.out.println("a:"+a.toString()+"\nb:"+b.toString());      a.peel(1);      b.peel(2);  } }
结果输出:
a:Banana@610455d6 b:Banana@511d50c0 Banana@610455d6 1 Banana@511d50c0 2
同一个类的两个对象,调用类的同一方法,是如何知道是被哪个对象调用的呢?
为了能用简便,面向对象的语法来编写代码——即发送消息给对象,编译器做了一些幕后工作,它暗自把“所操作对象的引用”作为第一个参数传递给peel()。所以a,b调用的两个方法就变成了这样:
Banana.peel(a,1); Banana.peel(b,2);
从结果输出也能看出来编译器将所操作对象的引用传递给了peel()方法。
this关键字用于在方法内部获得对当前对象的引用。表示对"调用方法的那个对象"的引用。
this的用法和其他对象引用没有不同。
注意:在方法内部调用同一个类的另一个方法不必使用this,直接调用即可。
  public class ThisDemo {    int i = 0;    ThisDemo increment(){        i++;        //返回对当前对象的引用        return this;    }    void print(){        System.out.println("i="+i);    }  public static void main(String[] args) {    ThisDemo thisDemo = new ThisDemo();    thisDemo.increment().increment().increment().increment().print(); } 构造器中调用构造器
一个类中有多个构造器,为避免重复代码,可能在一个构造器中调用另一个构造器。使用this关键字调用其他构造器。
除构造器之外,编译器禁止在其他任何方法中调用构造器。 static的含义
static(静态)方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来可以。
static方法的主要用途:可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。(很像全局方法,但是在Java中禁止使用全局方法,但在类中置入static方法就可以访问其他static方法和static域)由于不存在this,所以静态方法不是通过"向对象发送消息"的方式来完成的。 清理:终结处理和垃圾回收
Java有垃圾回收器负责回收无用对象占据的

5.1用构造器确保初始化
跟C++中的构造器类似
初始化和创建绑定在一起,密不可分。
构造器没有返回值;是默认的static函数
5.2方法重载
和C++中类似
一个方法的签名本质是由原来的方法名,参数列表和返回值构成的,所以虽然是重载了的方法,其本质还是各不相同的一组方法。
基本类型的重载
实参类型小于形参类型:自动提升
实参类型大于形参类型:需要手动进行强制类型转换
5.3默认构造器
如果自己定义了构造器,就不会自动产生默认构造器了
5.4this指针
其实类的成员函数都有一个参数this,用来指向当前对象
static的含义
static用来表示类变量和方法,区别于成员变量和方法。
static修饰过的变量只初始化一次
static修饰过的方法没有this指针。
5.5终结处理和垃圾回收
这一部分看我之前的一篇文章[编译原理]垃圾收集即可了解。将来会在讲到JVM的时候更详细地讲述
概念
自适应技术:自适应的、分代的、停止-复制、标记清扫式的垃圾回收器
JIT:及时编译
5.6成员初始化
局部变量不会自动初始化
局部变量自动初始化(引用类型初始化为null)
5.7构造器初始化
自动初始化在构造器初始化之前进行,如下代码i自动初始化位0,然后由构造器初始化为7.
public class Counter {
    int i;
    Counter(){ i = 7; }
}
1
2
3
4
类中成员变量的初始化顺序同其在类中排列的顺序相同。
静态数据的初始化:基本数据类型位其标准值,引用则为null
对象创建过程:
类初始化(不是对象初始化),当类被首次访问时
载入类的class文件,静态初始化进行
为对象分配内存
基本数据类型置为基本值,引用置为null
执行所有出现于字段定义处的初始化动作
执行构造器
5.8数组初始化
就谈一下可变参数列表
形如:public void function(Object ...args)
实现原理就是一个Object(java中的泛型也是用Object擦除实现的)类型的数组,用的时候需要强制类型转换
5.9枚举
java的枚举好像要比C语言中的枚举复杂不少
jdk5之后才出现枚举,之前需要定义整数常量集来实现(跟C语言的比较像啊)。enum本质是类,自动实现toString方法和ordinal方法,后者给出美剧常量的声明顺序。


Java数据存储
对象存在何处,C++认为效率控制是最重要的议题,所以给程序员提供了选择的权利。为了追求最大的执行速度,对象的存储空间与生命周期可以在编写程序时确定,可以将对象置于堆栈,限域变量或者静态存储区。
第二种方式是在被称为堆的内存池中动态创建对象,在这种方式中,知道运行时才知道需要多少对象。
对象生命周期。对于允许在堆栈中创建对象的语言,编译器可以确定对象存活的时间,并自动销毁它。而对于堆上创建的对象,编译器则对它的生命周期一无所知。
Java对象存储位置
寄存器。最快的存储区,不受控制
堆栈。位于通用RAM中,通过堆栈指针可以从处理器哪里直接获得支持。堆栈支持向下移动,则分配新的内存;向上移动,则释放那些内存。这是一种快速有效的分配存储方式,仅次于寄存器。创建程序时Java必须知道堆栈内所有项的生命周期,对象引用存于其中,Java对象并不存储其中。
堆。一种通用的内存池,也位于RAM,用于存放所有的Java对象。相比栈,编译器不需要知道堆中数据的存活时间,具有很大的灵活性。但这种灵活性也有很大代价,用堆进行存储分配和清理比栈需要更长的时间。
常量存储。常量值通常直接存放在程序代码内部,因为常量永远不会改变。
非RAM存储。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。两个基本粒子是流对象和持久化对象。
特例:基本类型。对于一些基本类型,Java采用和C++相同方法,不用new创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
特例:Java中的数组。当创建一个数组对象,实际创建了一个引用数组,并且每个引用都会自动被初始化为一个特定值null。当然,还可以创建存放基本数据类型的数组,编译器也能确保这种数组的初始化。
finalize方法
Java垃圾回收器只知道如何释放那些由new分配的内存,对于非使用new创建的特殊内存区域(比如Java本地方法,调用C/C++),垃圾回收器不能有效的进行释放。
在这种情况下,Java提供了finalize方法。
这里与C++的析构函数进行对比。在C++中,析构函数使得对象一定会被销毁,而Java中调用finalize并非总能被垃圾回收。
对象可能不被垃圾回收
垃圾回收并不等于析构
垃圾回收只与内存有关
使用垃圾回收器的唯一原因就是为了回收程序不再使用的内容,所以对于垃圾回收有关的任何行为(尤其finalize方法),它们也必须与内存及其回收有关。
无论对象如何创建,垃圾回收期都会负责释放对象占据的所有内存。所以一般情况下都不需要通过finalize方法释放。除非某些特殊情况,例如本地方法调用C/C++,malloc的内存需要在finalize方法中调用free释放。
所以,不要过多的使用finalize方法。
finalize方法可以用来验证对象终结条件。
比如对于一批书籍,new Book生成的对象释放的条件是这本书需要录入系统,但如果存在某些书籍在没有录入系统的情况下被回收了可以使用finalize方法查出异常。
protected void finalize() {
    if(checkedOut)
        System.out.println("Error: check out");
}
1
2
3
4
初始化
静态数据的初始化
静态数据初始化只有在必要时刻才会进行,此后,静态对象不会再次被初始化。
初始化的顺序是先静态对象,而后是非静态对象。比如创建了一个Dog的类:
即使没有显式使用static,构造器实际也是静态方法。因此首次创建Dog对象时,或者Dog类的静态方法/域被访问时,Java解释器必须查找类路径,定位Dog.class文件
载入Dog.class,有关静态初始化的所有动作都会执行,并且静态初始化仅执行一次
当用new Dog()创建对象时,在堆上为Dog对象分配足够的存储空间
这块存储空间清零(基本类型赋默认值,引用赋null)
执行所有出现于字段定义处的初始化动作
执行构造器
数组初始化
所有数组都有一个固定成员,通过它可以获得数组内包含了多少个元素,但不能进行修改。length
垃圾回收机制
一般程序语言在堆上分配对象的代价十分高昂,然后对于Java,垃圾回收器对于提高对象的创建速度具有明显效果。Java从堆分配空间的速度,可以和其他语言从栈中分配空间的速度相媲美。
打个比方,C++的堆可以想象一个院子,里面每个对象都负责管理自己的地盘,一段时间后,对象可能被销毁,但地盘必须加以重用;而对于Java,堆更像一个传送带,每分配一个新对象,它就往前移动一格,这意味着对象存储空间分配速度非常快,效率可以比得上C++栈上分配空间。
Java的堆并不完全像传送带那样工作,不然会导致频繁的内存页面调度,严重影响性能。其中的秘密在于垃圾回收器的介入,当它工作时,将一面回收空间,一面使堆中的对象紧凑,这样堆指针很容易移动到更靠近传送带的开始处。
现在理解其他语言的垃圾回收机制,引用计数器是一种简单但速度很慢的方法。每个对象都有一个引用计数器,当有引用连接对象时,引用计数器+1,当引用离开作用域或置null时,-1。虽然引用计数开销不大,但这项开销在整个程序生命周期持续发生,垃圾回收器在含有全部对象的列表遍历,某个对象引用为0是释放空间。这种方法有个缺陷,如果对象之间存在循环引用,可能出现“对象应该被回收,但引用计数不为0”。所以引用计数这种方式常常用来说明垃圾回收机制,并未被真正使用。
在一些更快的模式中,垃圾回收器并非基于引用计数技术,它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链可能穿过数个对象层次。因此,从堆栈和静态存储区开始,遍历所有的引用,就能找到“活”的对象。
在这种方式下,Java虚拟机采用了一种自适应的垃圾回收技术。至于如何处理存活的对象,取决于不同的Java虚拟机实现。有一种做法“停止-复制(stop-and-copy)”先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾。
对于这种复制式回收器而言,效率很低。
两个堆倒腾,需要双倍空间。某些虚拟机解决方法是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
复制本身。程序稳定后可能只产生了少量垃圾,甚至没有垃圾。复制必然会造成浪费。解决方法是如果没有新垃圾产生,会切换到另一种模式(自适应)“标记-清扫(mark-and-sweep)”
“标记-清扫”思路同样是从堆栈和静态存储区触发,遍历所有引用,找到存活对象进行标记。标记完成后对没有标记的对象进行清理。这样剩下的堆空间可能不连续,垃圾回收器希望得到连续空间的话需要重新整理剩下的对象。
“停止-复制”,“标记-清扫”都必须在程序暂停的情况下进行。
Java虚拟机中,内存分配以较大的“块”为单位,如果对象较大,会占用单独的“块”。“停止-复制”要求释放对象前把所有存活对象从旧堆复制到新堆,这会造成大量内存复制行为。有了块以后,垃圾回收器在回收时可以往废弃的块拷贝对象。每个块都有相应的代数来记录是否存活,某个块被引用,代数增加。垃圾回收器对上次回收动作之后新分配块进行整理,大型对象不会被复制,小型对象的那些块则被复制并整理。如果所有对象都很稳定,垃圾回收器的效率低的话,就切换到“标记-清扫”,如果堆中出现很多碎片,则切换回“停止-复制”方式,这就是“自适应”技术。
Java虚拟机有很多附加技术用以提升速度,尤其与加载器操作相关的,被称为“即时”(Just-In-Time,JIT)编译器的技术。这种技术把程序全部或部分翻译成本地机器码,程序运行速度得以提升。当需要装载某个类,编译器首先找到其class文件,然后将该类的字节码载入内存。此时,有两种方法选择
即时编译器编译所有代码。缺陷很明显:加载动作散落在整个程序生命周期,累计起来耗时更大;增加可执行代码的长度,导致页面调度,降低程序速度
惰性评估,即时编译器只需要在必要的时候编译代码,这样不执行的代码不会被JIT编译。
新版Jdk中的Java Hotspot技术采用类似方法,代码每次执行多会做一些优化,执行次数越多,速度就越快。

以构造函数(constructor)确保初始化的进行
如果某个class具备构造函数,Java便会在对象生成之际,使用者有能力加以操作之前,自动调用其构造函数,于是便能名确保初始化动作一定被执行。
二.函数重载(Method overloading)
1. 区分重载函数
由于只能从函数名和函数的引数列来区分两个函数,而重载函数具有相同的函数名称,所以每个重载函数都必须具备独一无二的引数列。
2. Default构造函数
1) default构造函数是一种不带任何引数的构造函数。如果你所开发的class不具任何构造函数,编译器会自动为你生成一个default构造函数。
2) 如果你自行定义了任何一个构造函数(不论有无引数),编译器就不会为你生成default构造函数。
3) 如果定义了一个class,如
class Bush{
Bush(int I){}
}

当想用new Bush();来产生class的实例时,会产生错误。因为在定义class时已定义了构造函数,所以编译器就不会为class生成default构造函数。当我们用new Bush()来产生实例时,会尝试调用default构造函数,但在class中没有default构造函数,所以会出错。如:
class Sundae
{
Sundae(int i) {}
}
public class IceCream
{
public static void main(String&#91;&#93; args)
{
//Sundae x = new Sundae();会编译出错,无构造函数Sundae()
Sundae y = new Sundae(1);
}
}

*:在定义一个class时,如果定义了自己的构造函数,最好同时定义一个default构造函数
3. 关键字this
1) this仅用于函数之内,能取得“唤起此一函数“的那个object reference。
2) 在构造函数中,通过this可以调用同一class中别的构造函数,如
public class Flower{
Flower (int petals

构造器是一类特殊的方法,它没有返回值,采用与类相同的名称,“每个方法首字母小写”的编码风格并不适用于构造器。

区分重载的规则很简单:每个重载的方法必须有一个独一无二的参数类型列表。

如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。

【5.4 this关键字】
this关键词只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。
如果在方法内部调用同一类的另一个方法,不必使用this,直接调用即可。
如果要将当前对象传递给外部的方法,this关键词就很有用。

可能为一个类写了多个构造器,想在一个构造其中调用灵一个构造器,以避免重复代码。this关键字可以做到这一点。
通常写this的时候,表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就有了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用。
尽管可以用this调用一个构造器,但不能调用两个。此外,必须将构造器置于最起始处,否则编译器会报错。
除了构造器外,编译器禁止在其他任何地方调用构造器。

【5.5 清理:终结处理和垃圾回收】


【5.6 成员初始化】
Java尽力保证:所有变量在使用前都能得到恰当的初始化。
类的每个基本类型数据成员保证都会有一个初始值。
在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。
要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。
在类的内部,变量定义的顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
静态初始化只有在必要的时刻才会进行。只有在第一个对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后,静态对象不会再次被初始化。
初始化的顺序是,先静态对象(如果他们尚未初始化),而后是非静态对象。
Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”)。

【5.7 数组初始化】

猜你喜欢

转载自zhyp29.iteye.com/blog/2306460