本章开始介绍构建器,它的作用是担保初始化的正确实现。对构建器的定义要涉及函数过载的概念(因为可能同时有几个构建器)。随后要讨论的是清除过程,它并非肯定如想象的那么简单。用完一个对象后,通常 可以不必管它,垃圾收集器会自动介入,释放由它占据的内存。这里详细探讨了垃圾收集器以及它的一些特点。在这一章的最后,我们将更贴近地观察初始化过程:自动成员初始化、指定成员初始化、初始化的顺 序、static(静态)初始化以及数组初始化等等。
1. 用构建器自动初始化
一旦创建一个对象:new XXX();就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同!和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。
在Java 中,定义和初始化属于统一的概念——两者缺一不可。
构建器属于一种较特殊的方法类型,因为它没有返回值。这与void 返回值存在着明显的区别。对于void 返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。
2.方法过载
2.1 区分过载方法
即使自变量的顺序也足够我们区分两个方法(尽管我们通常不愿意采用这种方法,因为它会产生难以维护的代码):
//: OverloadingOrder.java
// Overloading based on the order of
// the arguments.
public class OverloadingOrder {
static void print(String s, int i) {
System.out.println(
"String: " + s +
", int: " + i);
}
static void print(int i, String s) {
System.out.println(
"int: " + i +
", String: " + s);
}
public static void main(String[] args) {
print("String first", 11);
print(99, "Int first");
}
}
两个print()方法有完全一致的自变量,但顺序不同,可据此区分它们。
2.2 返回值过载
为什么只有类名和方法自变量列出?为什么不根据返回值对方法加以区分?比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的:void f() {}
int f() {}
若编译器可根据上下文(语境)明确判断出含义,比如在int x=f()中,那么这样做完全没有问题。然而,我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我们关心的不是返回值,而是方法调用的其他效果。所以假如我们象下面这样调用方法:
f();
Java 怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分过载的方法。
2.3 默认构建器
建器的类,则编译程序会帮我们自动创建一个默认构建器。例如:
//: DefaultConstructor.java
class Bird {
int i;
}
public class DefaultConstructor {
public static void main(String[] args) {
Bird nc = new Bird(); // default!
}
}
对于下面这一行:new Bird();
它的作用是新建一个对象,并调用默认构建器——即使尚未明确定义一个象这样的构建器。若没有它,就没有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构建器(无论是否有自变量),编译程序都不会帮我们自动合成一个:
class Bush {
Bush(int i) {}
Bush(double d) {}
}
现在,假若使用下述代码:
new Bush();
编译程序就会报告自己找不到一个相符的构建器。就好象我们没有设置任何构建器,编译程序会说:“你看来似乎需要一个构建器,所以让我们给你制造一个吧。”但假如我们写了一个构建器,编译程序就会说:“啊,你已写了一个构建器,所以我知道你想干什么;如果你不放置一个默认的,是由于你打算省略它。”
2.4 this 关键字
可为已调用了其方法的那个对象生成相应的句柄。可象对待其他任何对象句柄一样对待这个句柄。但要注意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用this。只需简单地调用那个方法即可。当前的this 句柄会自动应用于其他方法。所以我们能使用下面这样的代码:
class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ } //从自己某个类的另一个方法内部调用一个类方法,就不必使用this
} //在pit()内部,我们可以说this.pick(),但事实上无此必要。编译器能帮我们自动完成。
在return 语句中使用。
//: Leaf.java
// Simple use of the "this" keyword
public class Leaf {
private int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print(); //对同一个对象执行多项操作。
}
}
由于increment()通过this 关键字返回当前对象的句柄,所以可以方便地对同一个对象执行多项操作。public class Flower {
private int petalCount = 0;
private String s = new String("null");
Flower(int petals) {
petalCount = petals;
System.out.println("Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss) {
System.out.println("Constructor w/ String arg only, s=" + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//! this(s); // Can't call two!
this.s = s; // Another use of "this"
System.out.println("String & int args");
}
Flower() {
this("hi", 47);
System.out.println("default constructor (no args)");
}
void print() {
//! this(11); // Not inside non-constructor!
System.out.println("petalCount = " + petalCount + " s = "+ s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.print();
}
}
其中,构建器Flower(String s,int petals)向我们揭示出这样一个问题:尽管可用this 调用一个构建器,但不可调用两个。除此以外,构建器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。这个例子也向大家展示了this 的另一项用途。由于自变量s 的名字以及成员数据s 的名字是相同的,所以会出现混淆。为解决这个问题,可用this.s 来引用成员数据。经常都会在Java 代码里看到这种形式的应用,在print()中,我们发现编译器不让我们从除了一个构建器之外的其他任何方法内部调用一个构建器。
2.5 static 的含义
方法最基本的意义。它就好象我们创建一个全局函数的等价物(在C 语言中)。除了全局函数不允许在Java中使用以外,若将一个static 方法置入一个类的内部,它就可以访问其他static 方法以及static 字段。
3. 清除:收尾和垃圾收集
程序员都知道“初始化”的重要性,但通常忘记清除的重要性。毕竟,谁需要来清除一个int 呢?但是对于库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java 可用垃圾收集器回收由不再使用的对象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没有使用new。垃圾收集器只知道释放那些由new 分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java 提供了一个名为finalize()的方法,可为我们的类定义它。在理想情况下,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作。
但也是一个潜在的编程陷阱,因为有些程序员(特别是在C++开发背景的)刚开始可能会错误认为它就是在C++中为“破坏器”(Destructor)使用的finalize()——破坏(清除)一个对象的时候,肯定会调用这个函数。但在这里有必要区分一下C++和Java 的区别,因为C++的对象肯定会被清除(排开编程错误的因素),而Java 对象并非肯定能作为垃圾被“收集”去。或者换句话说:垃圾收集并不等于“破坏”!
若能时刻牢记这一点,踩到陷阱的可能性就会大大减少。它意味着在我们不再需要一个对象之前,有些行动是必须采取的,而且必须由自己来采取这些行动。Java 并未提供“破坏器”或者类似的概念,所以必须创建一个原始的方法,用它来进行这种清除。例如,假设在对象创建过程中,它会将自己描绘到屏幕上。如果不从屏幕明确删除它的图像,那么它可能永远都不会被清除。若在finalize()里置入某种删除机制,那么假设对象被当作垃圾收掉了,图像首先会将自身从屏幕上移去。但若未被收掉,图像就会保留下来。所以要记住的第二个重点是:我们的对象可能不会当作垃圾被收掉!
有时可能发现一个对象的存储空间永远都不会释放,因为自己的程序永远都接近于用光空间的临界点。若程序执行结束,而且垃圾收集器一直都没有释放我们创建的任何对象的存储空间,则随着程序的退出,那些资源会返回给操作系统。这是一件好事情,因为垃圾收集本身也要消耗一些开销。如永远都不用它,那么永远也不用支出这部分开销。
3 . 1 finalize ( ) 用途何在
大家可能已相信了自己应该将finalize()作为一种常规用途的清除方法使用。它有什么好处呢?
要记住的第三个重点是:
垃圾收集只跟内存有关!
垃圾收集器存在的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾收集有关的任何活动来说,其中最值得注意的是finalize()方法,它们也必须同内存以及它的回收有关。但这是否意味着假如对象包含了其他对象,finalize()就应该明确释放那些对象呢?答案是否定的——垃圾收集器会负责释放所有对象占据的内存,无论这些对象是如何创建的。它将对finalize()的需求限制到特殊的情况。在这种情况下,我们的对象可采用与创建对象时不同的方法分配一些存储空间。但大家或许会注意到,Java 中的所有东西都是对象,所以这到底是怎么一回事呢?之所以要使用finalize(),看起来似乎是由于有时需要采取与Java 的普通方法不同的一种方法,通过分内存来做一些具有C 风格的事情。这主要可以通过“固有方法”来进行,它是从Java 里调用非Java 方法的
一种方式(固有方法的问题在附录A 讨论)。C 和C++是目前唯一获得固有方法支持的语言。但由于它们能调用通过其他语言编写的子程序,所以能够有效地调用任何东西。在非Java 代码内部,也许能调用C 的malloc()系列函数,用它分配存储空间。而且除非调用了free(),否则存储空间不会得到释放,从而造成内存“漏洞”的出现。当然,free()是一个C 和C++函数,所以我们需要在finalize()内部的一个固有方法中调用它。
读完上述文字后,大家或许已弄清楚了自己不必过多地使用finalize()。这个思想是正确的;它并不是进行
普通清除工作的理想场所。那么,普通的清除工作应在何处进行呢?
3.2 必须执行清除
为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这听起来似乎很容易做到,但却与C++“破坏器”的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对\\\\\\\\\\\象都“应该”破坏。若将C++对象创建成一个本地对象,比如在堆栈中创建(在Java 中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的、创建这个对象的作用域的末尾进行。若对象是用new 创建的(类似于Java),那么当程序员调用C++的delete 命令时(Java 没有这个命令),就会调用相应的破坏器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”,另外还包括对象的其他部分永远不会得到清除。
相反,Java 不允许我们创建本地(局部)对象——无论如何都要使用new。但在Java 中,没有“delete”命令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说正是由于存在垃圾收集机制,所以Java 没有破坏器。然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调用finalize(),所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java 中的一个方法。它等价于C++的破坏器,只是没后者方便。
//: Garbage.java
// Demonstration of the garbage
// collector and finalization
class Chair {
static boolean gcrun = false;
static boolean f = false;
static int created = 0;
static int finalized = 0;
int i;
Chair() {
i = ++created;
if(created == 47)
System.out.println("Created 47");
}
protected void finalize() {
if(!gcrun) {
gcrun = true;
System.out.println(
"Beginning to finalize after " +
created + " Chairs have been created");
}
if(i == 47) {
System.out.println(
"Finalizing Chair #47, " +
"Setting flag to stop Chair creation");
f = true;
}
finalized++;
if(finalized >= created)
System.out.println(
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()方法,以便进行收尾工作。这些方法都可在Java 1.0 中使用,但通过使用“after”自变量而调用的runFinalizersOnExit()方法却只有Java 1.1 及后续版本提供了对它的支持。注意可在程序执行的任何时候调用这个方法,而且收尾程序的执行与垃圾收集器是否运行是无关的。
4 成员初始化
Java 尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变量,这一保证就通过编译期的出错提示表现出来。因此,如果使用下述代码:
void f() {
int i;
i++;
}
就会收到一条出错提示消息,告诉你i 可能尚未初始化。当然,编译器也可为i 赋予一个默认值,但它看起来更象一个程序员的失误,此时默认值反而会“帮倒忙”。若强迫程序员提供一个初始值,就往往能够帮他/她纠出程序里的“bug”。然而,若将基本类型(主类型)设为一个类的数据成员,情况就会变得稍微有些不同。由于任何方法都可以初始化或使用那个数据,所以在正式使用数据前,若还是强迫程序员将其初始化成一个适当的值,就可能不是一种实际的做法。然而,若为其赋予一个垃圾值,同样是非常不安全的。因此,一个类的所有基本类型数据成员都会保证获得一个初始值。可用下面这段小程序看到这些值://: 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 . 1 规定初始化
如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部定义变量的同时也为其赋值(注意在C++里不能这样做)。在下面,Measurement 类内部的字段定义已发生了变化,提供了初始值:
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)的一个错误。甚至可通过调用一个方法来提供初始值:
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 . 2 构建器初始化
可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建器进入之前就会发生。因此,假如使用下述代码:
class Counter {
int i;
Counter() { i = 7; }
// . . .
那么i 首先会初始化成零,然后变成7。对于所有基本类型以及对象句柄,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构建器任何特定的
场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证.
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
}
} ///:~
在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);
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) 执行构建器。这实际可能要求进行相当多的操作,特别是在涉及继承的时候。
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 提供了一种类似的语法格式。下面是一个例子:
//: 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 章),必须采用这一语法格式。
5 数组初始化
数组代表一系列对象或者基本数据类型,所有相同的类型都封装到一起——采用一个统一的标识符名称。数组的定义和使用是通过方括号索引运算符进行的([])。为定义一个数组,只需在类型名后简单地跟随一对空方括号即可:int[] al;
也可以将方括号置于标识符后面,获得完全一致的结果:
int al[];
编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“句柄”的问题上。此时,我们拥有的一切就是指向数组的一个句柄,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。存储空间的分配(等价于使用new)将由编译器在这种情况下进行。例如:
int[] a1 = { 1, 2, 3, 4, 5 };
那么为什么还要定义一个没有数组的数组句柄呢?
int[] a2;
事实上在Java 中,可将一个数组分配给另一个,所以能使用下述语句:
a2 = a1;
我们真正准备做的是复制一个句柄,就象下面演示的那样:
//: Arrays.java
// Arrays of primitives.
public class Arrays {
public static void main(String[] args) {
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2;
a2 = a1;
for(int i = 0; i < a2.length; i++)
a2[i]++;
for(int i = 0; i < a1.length; i++)
prt("a1[" + i + "] = " + a1[i]);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
大家看到a1 获得了一个初始值,而a2 没有;a2 将在以后赋值——这种情况下是赋给另一个数组。这里也出现了一些新东西:所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是length。与C 和C++类似,由于Java 数组从元素0 开始计数,所以能索引的最大元素编号是“length-1”。如超出边界,C 和C++会“默默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。然而,Java 可保留我们这受这一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“违例”,这是第9 章的主题)。当然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。但考虑到因特网访问的安全,以及程序员的编程效率,Java 设计人员还是应该把它看作是值得的。程序编写期间,如果不知道在自己的数组里需要多少元素,那么又该怎么办呢?此时,只需简单地用new 在数组里创建元素。在这里,即使准备创建的是一个基本数据类型的数组,new 也能正常地工作(new 不会创建非数组的基本类型):
//: ArrayNew.java
// Creating arrays with new.
import java.util.*;
public class ArrayNew {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
int[] a;
a = new int[pRand(20)];
prt("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
prt("a[" + i + "] = " + a[i]);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
由于数组的大小是随机决定的(使用早先定义的pRand()方法),所以非常明显,数组的创建实际是在运行期间进行的。除此以外,从这个程序的输出中,大家可看到基本数据类型的数组元素会自动初始化成“空”值(对于数值,空值就是零;对于char,它是null ;而对于boolean,它却是false)。当然,数组可能已在相同的语句中定义和初始化了,如下所示:int[] a = new int[pRand(20)];
若操作的是一个非基本类型对象的数组,那么无论如何都要使用new。在这里,我们会再一次遇到句柄问题,因为我们创建的是一个句柄数组。请大家观察封装器类型Integer,它是一个类,而非基本数据类型:
//: ArrayClassObj.java
// Creating an array of non-primitive objects.
import java.util.*;
public class ArrayClassObj {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
Integer[] a = new Integer[pRand(20)];
prt("length of a = " + a.length);
for(int i = 0; i < a.length; i++) {
118
a[i] = new Integer(pRand(500));
prt("a[" + i + "] = " + a[i]);
}
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
在这儿,甚至在new 调用后才开始创建数组:
Integer[] a = new Integer[pRand(20)];
它只是一个句柄数组,而且除非通过创建一个新的Integer 对象,从而初始化了对象句柄,否则初始化进程不会结束:
a[i] = new Integer(pRand(500));
但若忘记创建对象,就会在运行期试图读取空数组位置时获得一个“违例”错误。下面让我们看看打印语句中String 对象的构成情况。大家可看到指向Integer 对象的句柄会自动转换,从而产生一个String,它代表着位于对象内部的值。
亦可用花括号封闭列表来初始化对象数组。可采用两种形式,第一种是Java 1.0 允许的唯一形式。第二种(等价)形式自Java 1.1 才开始提供支持:
//: ArrayInit.java
// Array initialization
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
new Integer(1),
new Integer(2),
new Integer(3),
};
// Java 1.1 only:
Integer[] b = new Integer[] {
new Integer(1),
new Integer(2),
new Integer(3),
};
}
} ///:~
这种做法大多数时候都很有用,但限制也是最大的,因为数组的大小是在编译期间决定的。初始化列表的最后一个逗号是可选的(这一特性使长列表的维护变得更加容易)。数组初始化的第二种形式(Java 1.1 开始支持)提供了一种更简便的语法,可创建和调用方法,获得与C 的“变量参数列表”(C 通常把它简称为“变参表”)一致的效果。这些效果包括未知的参数(自变量)数量以及未知的类型(如果这样选择的话)。由于所有类最终都是从通用的根类Object 中继承的,所以能创建一个方法,令其获取一个Object 数组,并象下面这样调用它:
//: VarArgs.java
// Using the Java 1.1 array syntax to create
// variable argument lists
class A { int i; }
119
public class VarArgs {
static void f(Object[] x) {
for(int i = 0; i < x.length; i++)
System.out.println(x[i]);
}
public static void main(String[] args) {
f(new Object[] {
new Integer(47), new VarArgs(),
new Float(3.14), new Double(11.11) });
f(new Object[] {"one", "two", "three" });
f(new Object[] {new A(), new A(), new A()});
}
} ///:~
此时,我们对这些未知的对象并不能采取太多的操作,而且这个程序利用自动String 转换对每个Object 做一些有用的事情。在第11 章(运行期类型标识或RTTI),大家还会学习如何调查这类对象的准确类型,使自己能对它们做一些有趣的事情。5 . 1 多维数组
在Java 里可以方便地创建多维数组:
//: MultiDimArray.java
// Creating multidimensional arrays.
import java.util.*;
public class MultiDimArray {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
for(int i = 0; i < a1.length; i++)
for(int j = 0; j < a1[i].length; j++)
prt("a1[" + i + "][" + j +
"] = " + a1[i][j]);
// 3-D array with fixed length:
int[][][] a2 = new int[2][2][4];
for(int i = 0; i < a2.length; i++)
for(int j = 0; j < a2[i].length; j++)
for(int k = 0; k < a2[i][j].length;
k++)
prt("a2[" + i + "][" +
j + "][" + k +
"] = " + a2[i][j][k]);
// 3-D array with varied-length vectors:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(5)];
}
for(int i = 0; i < a3.length; i++)
for(int j = 0; j < a3[i].length; j++)
for(int k = 0; k < a3[i][j].length;
k++)
prt("a3[" + i + "][" +
j + "][" + k +
"] = " + a3[i][j][k]);
// Array of non-primitive objects:
Integer[][] a4 = {
{ new Integer(1), new Integer(2)},
{ new Integer(3), new Integer(4)},
{ new Integer(5), new Integer(6)},
};
for(int i = 0; i < a4.length; i++)
for(int j = 0; j < a4[i].length; j++)
prt("a4[" + i + "][" + j +
"] = " + a4[i][j]);
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);
}
for(int i = 0; i < a5.length; i++)
for(int j = 0; j < a5[i].length; j++)
prt("a5[" + i + "][" + j +
"] = " + a5[i][j]);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
用于打印的代码里使用了length,所以它不必依赖固定的数组大小。第一个例子展示了基本数据类型的一个多维数组。我们可用花括号定出数组内每个矢量的边界:
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
每个方括号对都将我们移至数组的下一级。第二个例子展示了用new 分配的一个三维数组。在这里,整个数组都是立即分配的:
int[][][] a2 = new int[2][2][4];
但第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(5)];
}
对于第一个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 里置了一个有趣的值。