JAVA编程思想-第 4 章 初始化和清除

第 4 章 初始化和清除

“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。”
“初始化”和“清除”是这些安全问题的其中两个。许多 C 程序的错误都是由于程序员忘记初始化一个变量
造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。
C++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java 也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及 Java 如何提供它们的支持。
4.1 用构建器自动初始化
对于方法的创建,可将其想象成为自己写的每个类都调用一次 initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在 Java 中,由于提供了名为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构建器,那么在创建对象时,Java 会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以担保的!
接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java 里得到了应用:构建器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。
下面是带有构建器的一个简单的类(若执行这个程序有问题,请参考第3 章的“赋值”小节)。
//: SimpleConstructor.java

95
// Demonstration of a simple constructor
package c04;
class Rock {
 Rock() { // This is the constructor
 System.out.println("Creating Rock");
 }
}
public class SimpleConstructor {
 public static void main(String[] args) {
 for(int i = 0; i < 10; i++)
 new Rock();
 }
} ///:~
现在,一旦创建一个对象:
new Rock();
就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。
请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同!
和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构建器使用自己的自变量。如下所示:
class Rock {
 Rock(int i) {
 System.out.println(
 "Creating Rock number " + i);
 }
}
public class SimpleConstructor {
 public static void main(String[] args) {
 for(int i = 0; i < 10; i++)
 new Rock(i);
 }
}
利用构建器的自变量,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类 Tree 有一个构建器,它用一个整数自变量标记树的高度,那么就可以象下面这样创建一个 Tree 对象:
tree t = new Tree(12); // 12 英尺高的树
若Tree(int)是我们唯一的构建器,那么编译器不会允许我们以其他任何方式创建一个 Tree 对象。
构建器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对
initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java 中,定义和初始化属于统一的概念——两者缺一不可。
构建器属于一种较特殊的方法类型,因为它没有返回值。这与 void 返回值存在着明显的区别。对于void 返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。

4.2.1 区分过载方法

4.2.2 主类型的过载

4.2.3 返回值过载

我们发现编译器不让我们从除了一个构建器之外的其他任何方法内部调用一个构建器。
 

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

107
 "All " + finalized + " finalized");
 }
}
public class Garbage {
 public static void main(String[] args) {
 if(args.length == 0) {
 System.err.println("Usage: \n" +
 "java Garbage before\n or:\n" +
 "java Garbage after");
 return;
 }
 while(!Chair.f) {
 new Chair();
 new String("To take up space");
 }
 System.out.println(
 "After all Chairs have been created:\n" +
 "total created = " + 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 对象的创建过程。
所有这些都在main()的内部进行——在下面这个循环里:
while(!Chair.f) {
new Chair();
new String("To take up space");
}
大家可能会疑惑这个循环什么时候会停下来,因为内部没有任何改变 Chair.f 值的语句。然而,finalize()进程会改变这个值,直至最终对编号 47 的对象进行收尾处理。
每次循环过程中创建的 String 对象只是属于额外的垃圾,用于吸引垃圾收集器——一旦垃圾收集器对可用内存的容量感到“紧张不安”,就会开始关注它。
运行这个程序的时候,提供了一个命令行自变量“before”或者“after”。其中,“before”自变量会调用System.gc()方法(强制执行垃圾收集器),同时还会调用 System.runFinalization()方法,以便进行收尾
108
工作。这些方法都可在 Java 1.0 中使用,但通过使用“after”自变量而调用的 runFinalizersOnExit()方法却只有Java 1.1 及后续版本提供了对它的支持(注释③)。注意可在程序执行的任何时候调用这个方法,
而且收尾程序的执行与垃圾收集器是否运行是无关的。
③:不幸的是,Java 1.0 采用的垃圾收集器方案永远不能正确地调用finalize()。因此,finalize()方法
(特别是那些用于关闭文件的)事实上经常都不会得到调用。现在有些文章声称所有收尾模块都会在程序退
出的时候得到调用——即使到程序中止的时候,垃圾收集器仍未针对那些对象采取行动。这并不是真实的情况,所以我们根本不能指望finalize()能为所有对象而调用。特别地,finalize()在Java 1.0 里几乎毫无用处。
前面的程序向我们揭示出:在 Java 1.1 中,收尾模块肯定会运行这一许诺已成为现实——但前提是我们明确地强制它采取这一操作。若使用一个不是“before”或“after”的自变量(如“none”),那么两个收尾工作都不会进行,而且我们会得到象下面这样的输出:
Created 47
Beginning to finalize after 8694 Chairs have been created
Finalizing Chair #47, Setting flag to stop Chair creation
After all Chairs have been created:
total created = 9834, total finalized = 108
bye!
因此,到程序结束的时候,并非所有收尾模块都会得到调用(注释④)。为强制进行收尾工作,可先调用
System.gc(),再调用 System.runFinalization()。这样可清除到目前为止没有使用的所有对象。这样做一个稍显奇怪的地方是在调用runFinalization()之前调用gc(),这看起来似乎与 Sun 公司的文档说明有些抵触,它宣称首先运行收尾模块,再释放存储空间。然而,若在这里首先调用runFinalization(),再调用
gc(),收尾模块根本不会执行。
④:到你读到本书时,有些Java 虚拟机(JVM)可能已开始表现出不同的行为。
针对所有对象,Java 1.1 有时之所以会默认为跳过收尾工作,是由于它认为这样做的开销太大。不管用哪种方法强制进行垃圾收集,都可能注意到比没有额外收尾工作时较长的时间延迟。
4.4 成员初始化
Java 尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变量,这一保证就通过编译期的出错提示表现出来。因此,如果使用下述代码:
void f() {
int i;
i++;
}
就会收到一条出错提示消息,告诉你i 可能尚未初始化。当然,编译器也可为i 赋予一个默认值,但它看起来更象一个程序员的失误,此时默认值反而会“帮倒忙”。若强迫程序员提供一个初始值,就往往能够帮他/她纠出程序里的“臭虫”。
然而,若将基本类型(主类型)设为一个类的数据成员,情况就会变得稍微有些不同。由于任何方法都可以初始化或使用那个数据,所以在正式使用数据前,若还是强迫程序员将其初始化成一个适当的值,就可能不是一种实际的做法。然而,若为其赋予一个垃圾值,同样是非常不安全的。因此,一个类的所有基本类型数据成员都会保证获得一个初始值。可用下面这段小程序看到这些值:
//: InitialValues.java
// Shows default initial values
class Measurement {
109
 boolean t;
 char c;
 byte b;
 short s;
 int i;
 long l;
 float f;
 double d;
 void print() {
 System.out.println(
 "Data type Inital 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);
 }
}
public class InitialValues {
 public static void main(String[] args) {
 Measurement d = 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),没有数据打印出来。
稍后大家就会看到:在一个类的内部定义一个对象句柄时,如果不将其初始化成新对象,那个句柄就会获得一个空值。
4.4.1 规定初始化
如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部定义变量的同时也为其赋值(注意在C++里不能这样做,尽管C++的新手们总“想”这样做)。在下面,
Measurement 类内部的字段定义已发生了变化,提供了初始值:
110
class Measurement {
 boolean b = true;
 char c = 'x';
 byte B = 47;
 short s = 0xff;
 int i = 999;
 long l = 1;
 float f = 3.14f;
 double d = 3.14159;
 //. . .
亦可用相同的方法初始化非基本(主)类型的对象。若Depth 是一个类,那么可象下面这样插入一个变量并进行初始化:
class Measurement {
Depth o = new Depth();
boolean b = true;
// . . .
若尚未为o 指定一个初始值,同时不顾一切地提前试用它,就会得到一条运行期错误提示,告诉你产生了名为“违例”(Exception)的一个错误(在第 9 章详述)。
甚至可通过调用一个方法来提供初始值:
class CInit {
int i = f();
//...
}
当然,这个方法亦可使用自变量,但那些自变量不可是尚未初始化的其他类成员。因此,下面这样做是合法的:
class CInit {
int i = f();
int j = g(i);
//...
}
但下面这样做是非法的:
class CInit {
int j = g(i);
int i = f();
//...
}
这正是编译器对“向前引用”感到不适应的一个地方,因为它与初始化的顺序有关,而不是与程序的编译方式有关。
这种初始化方法非常简单和直观。它的一个限制是类型Measurement 的每个对象都会获得相同的初始化值。有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。

4.4.2 构建器初始化


可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建器进入之前就会发生。因此,假如使用下述代码:
class Counter {
int i;
Counter() { i = 7; }
// . . .
那么i 首先会初始化成零,然后变成 7。对于所有基本类型以及对象句柄,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构建器任何特定的场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。
⑤:相反,C++有自己的“构建器初始模块列表”,能在进入构建器主体之前进行初始化,而且它对于对象来说是强制进行的。参见《Thinking in C++》。
1. 初始化顺序
在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。例如:
//: OrderOfInitialization.java
// Demonstrates initialization order.
// When the constructor is called, to create a
// Tag object, you'll see a message:
class Tag {
 Tag(int marker) {
 System.out.println("Tag(" + marker + ")");
 }
}
class Card {
 Tag t1 = new Tag(1); // Before constructor
 Card() {
 // Indicate we're in the constructor:
 System.out.println("Card()");
 t3 = new Tag(33); // Re-initialize t3
 }
 Tag t2 = new Tag(2); // After constructor
 void f() {
 System.out.println("f()");
 }
 Tag t3 = new Tag(3); // At end
}
public class OrderOfInitialization {
 public static void main(String[] args) {
 Card t = new Card();
 t.f(); // Shows that construction is done
 }
112
} ///:~
在Card 中,Tag 对象的定义故意到处散布,以证明它们全都会在构建器进入或者发生其他任何事情之前得到初始化。除此之外,t3 在构建器内部得到了重新初始化。它的输入结果如下:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
因此,t3 句柄会被初始化两次,一次在构建器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个过载的构建器,它没有初始化 t3;同时在t3 的定义里并没有规定“默认”的初始化方式,那么会产生什么后果
呢?
2. 静态数据的初始化
若数据是静态的(static),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的句柄,那么除非新建一个对
象,并将句柄同它连接起来,否则就会得到一个空值(NULL)。
如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于static 值只有一个存储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。下面这个例子可将这个问题说更清楚一些:
//: StaticInitialization.java
// Specifying initial values in a
// class definition.
class Bowl {
 Bowl(int marker) {
 System.out.println("Bowl(" + marker + ")");
 }
 void f(int marker) {
 System.out.println("f(" + marker + ")");
 }
}
class Table {
 static Bowl b1 = new Bowl(1);
 Table() {
 System.out.println("Table()");
 b2.f(1);
 }
 void f2(int marker) {
 System.out.println("f2(" + marker + ")");
 }
 static Bowl b2 = new Bowl(2);
}
class Cupboard {
 Bowl b3 = new Bowl(3);
113
 static Bowl b4 = new Bowl(4);
 Cupboard() {
 System.out.println("Cupboard()");
 b4.f(2);
 }
 void f3(int marker) {
 System.out.println("f3(" + marker + ")");
 }
 static Bowl b5 = new Bowl(5);
}
public class StaticInitialization {
 public static void main(String[] args) {
 System.out.println(
 "Creating new Cupboard() in main");
 new Cupboard();
 System.out.println(
 "Creating new Cupboard() in main");
 new Cupboard();
 t2.f2(1);
 t3.f3(1);
 }
 static Table t2 = new Table();
 static Cupboard t3 = new Cupboard();
} ///:~
Bowl 允许我们检查一个类的创建过程,而Table 和 Cupboard 能创建散布于类定义中的Bowl 的 static 成员。注意在 static 定义之前,Cupboard 先创建了一个非static 的 Bowl b3。它的输出结果如下:
Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creating new 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) 执行构建器。正如第6 章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。
3. 明确进行的静态初始化
Java 允许我们将其他static 初始化工作划分到类内一个特殊的“static 构建从句”(有时也叫作“静态块”)里。它看起来象下面这个样子:
class Spoon {
 static int i;
 static {
 i = 47;
 }
 // . . .
尽管看起来象个方法,但它实际只是一个 static 关键字,后面跟随一个方法主体。与其他 static 初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个 static 成员时(即便从未生成过那个类的对象)。例如:
//: ExplicitStatic.java
// Explicit static initialization
// with the "static" clause.
class Cup {
 Cup(int marker) {
 System.out.println("Cup(" + marker + ")");
 }
 void f(int marker) {
 System.out.println("f(" + marker + ")");
 }
}
class Cups {
 static Cup c1;
 static Cup c2;
 static {
 c1 = new Cup(1);
 c2 = new Cup(2);
 }
 Cups() {
 System.out.println("Cups()");
115
 }
}
public class ExplicitStatic {
 public static void main(String[] args) {
 System.out.println("Inside main()");
 Cups.c1.f(99); // (1)
 }
 static Cups x = new Cups(); // (2)
 static Cups y = new Cups(); // (2) 
} ///:~
在标记为(1)的行内访问 static 对象c1 的时候,或在行(1)标记为注释,同时(2)行不标记成注释的时候,用于Cups 的 static 初始化模块就会运行。若(1)和(2)都被标记成注释,则用于 Cups 的static 初始化进程永远不会发生。
4. 非静态实例的初始化
针对每个对象的非静态变量的初始化,Java 1.1 提供了一种类似的语法格式。下面是一个例子:
//: Mugs.java
// Java 1.1 "Instance Initialization"
class Mug {
 Mug(int marker) {
 System.out.println("Mug(" + marker + ")");
 }
 void f(int marker) {
 System.out.println("f(" + marker + ")");
 }
}
public class Mugs {
 Mug c1;
 Mug c2;
 {
 c1 = new Mug(1);
 c2 = new Mug(2);
 System.out.println("c1 & c2 initialized");
 }
 Mugs() {
 System.out.println("Mugs()");
 }
 public static void main(String[] args) {
 System.out.println("Inside main()");
 Mugs x = new Mugs();
 }
} ///:~
大家可看到实例初始化从句:
 {
 c1 = new Mug(1);
 c2 = new Mug(2);
 System.out.println("c1 & c2 initialized");
 }
它看起来与静态初始化从句极其相似,只是static 关键字从里面消失了。为支持对“匿名内部类”的初始化
(参见第7 章),必须采用这一语法格式。

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值