欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51040234
随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。
许多 C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资
源会一直保留下去,极易产生资源(主要是内存)用尽的后果。
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'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);
}
} // /:~
输出如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Created47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
Beginningto finalize after 220 Chairs have been created
221
FinalizingChair #47, Setting flag to stop Chair creation
222
Afterall Chairs have been created:
totalcreated = 222, total finalized = 105
gc():
runFinalization():
All222 finalized
bye!
上面这个程序创建了许多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 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);
}
}
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'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.length;i++)
a2[i]++;
for(inti = 0; i <a1.length;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 <a.length;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 <x.length;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 <a1.length;i++)
for (intj = 0; j <a1[i].length;j++)
prt("a1[" +i +"][" +j +"] = " +a1[i][j]);
// 3-D array withfixed length:
int[][][]a2 =newint[2][2][4];
for (inti = 0; i <a2.length;i++)
for (intj = 0; j <a2[i].length;j++)
for (intk = 0; k <a2[i][j].length;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.length;i++) {
a3[i] =newint[pRand(5)][];
for (intj = 0; j <a3[i].length;j++)
a3[i][j] = newint[pRand(5)];
}
for (inti = 0; i <a3.length;i++)
for (intj = 0; j <a3[i].length;j++)
for (intk = 0; k <a3[i][j].length;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 <a4.length;i++)
for (intj = 0; j <a4[i].length;j++)
prt("a4[" +i +"][" +j +"] = " +a4[i][j]);
Integer[][]a5;
a5 =new Integer[3][];
for (inti = 0; i <a5.length;i++) {
a5[i] =new Integer[3];
for (intj = 0; j <a5[i].length;j++)
a5[i][j] = new Integer(i *j);
}
for (inti = 0; i <a5.length;i++)
for (intj = 0; j <a5[i].length;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 不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。
由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。