整理了Think in Java 3rd中所有关于初始化的段落,将其连贯成一片文章,分析Java中初始化类、变量、程序块加载问题。
author: ZJ 07-3-19
Blog: [url]http://zhangjunhd.blog.51cto.com/[/url]
1.基本类型数据的初始值
InitialValues.java
public
class
InitialValues {
boolean
t
;
char
c
;
byte
b
;
short
s
;
int
i
;
long
l
;
float
f
;
double
d
;
void
print(String s) {
System.
out
.println(s);
}
void
printInitialValues() {
print(
"boolean "
+
t
);
print(
"char "
+
c
);
print(
"byte "
+
b
);
print(
"short "
+
s
);
print(
"int "
+
i
);
print(
"long "
+
l
);
print(
"float "
+
f
);
print(
"double "
+
d
);
}
public
static
void
main(String[] args) {
InitialValues iv =
new
InitialValues();
iv.printInitialValues();
}
}
|
结果:
boolean false
char _
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
2.变量初始化
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧在任何方法(包括构造器)被调用之前得到初始化。看下面的代码:
OrderOfInitialzation.java(执行顺序在代码中已标出,按类标注,罗马字母标注主类中执行顺序。)
class
Tag {
Tag(
int
marker) {
System.
out
.println(
"Tag("
+ marker +
")"
);
}
}
class
Card {
Tag
t1
=
new
Tag(1);
//
Ⅰ①
Card() {
System.
out
.println(
"Card()"
);
//
Ⅰ④
t3
=
new
Tag(33);
//
Ⅰ⑤
}
Tag
t2
=
new
Tag(2);
//
Ⅰ②
void
f() {
System.
out
.println(
"f()"
);
//
Ⅱ⑥
}
Tag
t3
=
new
Tag(3);
//
Ⅰ③
}
public
class
OrderOfInitialzation {
public
static
void
main(String[] args) {
Card t =
new
Card();
//
Ⅰ
t.f();
//
Ⅱ
}
}
|
结果:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
3.静态数据初始化
看下面的代码:
StaticInitialization .java
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(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)
由输出可见,静态初始化只有在必要时刻才会进行。如果不创建Table 对象,也不引用Table.b1或Table.b2,那么静态的Bowl b1 和b2 永远都不会被创建。只有在第一个Table 对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后,静态对象不会再次被初始化。
初始化的顺序是先“静态”,(如果它们尚未因前面的对象创建过程而被初始化),后“非静态”。从输出结果中可以观察到这一点。
4.静态块的初始化
Java 允许你将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫作“静态块”)。与其他静态初始化动作一样,这段代码仅执行一次:当你首次生成这个类的一个对象时,或者首次访问属于那个类的一个静态成员时(即便从未生成过那个类的对象)。看下面的代码:
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()"
);
}
}
public
class
ExpilicitStatic {
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)
}
|
结果:
Inside main()
Cup(1)
Cup(2)
f(99)
无论是通过标为(1)的那行程序访问静态的 c1对象,还是把(1)注释掉,让它去运行标为(2) 的那行,Cups 的静态初始化动作都会得到执行。如果把(1)和(2)同时注释掉,Cups 的静态初始化动作就不会进行。此外,激活一行还是两行(2)代码都无关紧要,静态初始化动作只进行一次。
5.非静态实例初始化
看下面的代码:
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()"
);
new
Mugs();
System.
out
.println(
"===new Mugs again==="
);
new
Mugs();
}
}
|
结果:
Inside main()
Mug(1)
Mug(2)
c1&c2 initialized
Mugs()
===new Mugs again===
Mug(1)
Mug(2)
c1&c2 initialized
Mugs()
从结果可以看到,非静态的代码块被执行了2次。所以只要实例化一个类,该类中的非静态代码块就会被执行一次。
6.数组初始化
注意区分基本类型数据与类数据的初始化。看以下代码:
int
[] a;
a =
new
int
[rand.nextInt(20)];
|
与
Integer[] a =
new
Integer[rand.nextInt(20)];
for
(
int
i = 0; i < a.
length
; i++) {
a[i] =
new
Integer(rand.nextInt(500));
}
|
对于类数据类型的初始化,每个数组子成员都要重新new一下。
7.涉及继承关系的初始化
当创建一个导出类的对象时,该对象包含了一个基类的子对象。看下面代码:
class
Art {
Art() {
System.
out
.println(
"Art constructor"
);
}
}
class
Drawing
extends
Art {
Drawing() {
System.
out
.println(
"Drawing constructor"
);
}
}
public
class
Cartoon
extends
Drawing {
public
Cartoon() {
System.
out
.println(
"Cartoon constructor"
);
}
public
static
void
main(String[] args) {
new
Cartoon();
}
}
|
结果:
Art constructor
Drawing constructor
Cartoon constructor
可以发现,构建过程是从基类“向外”扩散的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。
如果类没有缺省的参数,或者想调用一个带参数的基类构造器,就必须用关键字super显示地调用基类构造器的语句,并且配以适当的参数列表。看下面代码:
class
Game {
Game(
int
i) {
System.
out
.println(
"Game constructor"
);
}
}
class
BoardGame
extends
Game {
BoardGame(
int
i) {
super
(i);
System.
out
.println(
"BoardGame constructor"
);
}
}
public
class
Chess
extends
BoardGame {
Chess() {
super
(11);
System.
out
.println(
"Chess constructor"
);
}
public
static
void
main(String[] args) {
new
Chess();
}
}
|
结果:
Game constructor
BoardGame constructor
Chess constructor
如果不在BoardGame()中调用基类构造器,编译器将无法找到符合Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中要做的第一件事。
8.构造器与多态
8.1构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用的,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊的任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。
8.2构造器内部的多态方法的行为
看下面代码:
abstract
class
Glyph {
abstract
void
draw();
Glyph() {
System.
out
.println(
"Glyph() before draw()"
);
draw();
System.
out
.println(
"Glyph() after draw()"
);
}
}
class
RoundGlyph
extends
Glyph {
private
int
radius
= 1;
RoundGlyph(
int
r) {
radius
= r;
System.
out
.println(
"RoundGlyph.RoundGlyph(),radius="
+
radius
);
}
void
draw() {
System.
out
.println(
"RoundGlyph.draw(),radius="
+
radius
);
}
}
public
class
PolyConstructors {
public
static
void
main(String[] args) {
new
RoundGlyph(5);
}
}
|
结果:
Glyph() before draw()
RoundGlyph.draw(),radius=0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius=5
在Glyph中,draw()方法是抽象的,这样设计是为了覆盖该方法。我们确实在RoungGlyph中强制覆盖了draw()。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这看起来似乎是我们的目的。但是如果看到输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius不是默认初始值1,而是0。
解决这个问题的关键是初始化的实际过程:
1)在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制零。
2)如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0。
3)按照声明的顺序调用成员的初始化方法。
4)调用导出类的构造器主体。
9.初始化及类的加载
看以下代码:
class
Tag {
Tag(
int
marker) {
System.
out
.println(
"Tag("
+ marker +
")"
);
}
}
class
Insect {
private
int
i
= 9;
protected
int
j
,
m
;
Insect() {
System.
out
.println(
"i = "
+
i
+
", j = "
+
j
);
j
= 39;
}
private
static
int
x1
= print(
"static Insect.x1 initialized"
);
static
int
print(String s) {
System.
out
.println(s);
return
47;
}
Tag
t1
=
new
Tag(1);
}
public
class
Beetle
extends
Insect {
private
int
k
= print(
"Beetle.k initialized"
);
public
Beetle() {
System.
out
.println(
"k = "
+
k
);
System.
out
.println(
"j = "
+
j
);
System.
out
.println(
"m = "
+
m
);
}
private
static
int
x2
= print(
"static Beetle.x2 initialized"
);
public
static
void
main(String[] args) {
System.
out
.println(
"Beetle constructor"
);
Beetle b =
new
Beetle();
}
Tag
t2
=
new
Tag(2);
}
|
结果:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
Tag(1)
i = 9, j = 0
Beetle.k initialized
Tag(2)
k = 47
j = 39
m = 0
你在Beetle 上运行Java 时,所发生的第一件事情就是你试图访问Beetle.main( ) (一个static 方法),于是加载器开始启动并找出 Beetle 类被编译的程序代码(它被编译到了一个名为Beetle .class 的文件之中)。在对它进行加载的过程中,编译器注意到它有一个基类(这是由关键字 extends 告知的),于是它继续进行加载。不管你是否打算产生一个该基类的对象,这都要发生。
如果该基类还有其自身的基类,那么第二个基类就会被加载,如此类推。接下来,根基类中的静态初始化(在此例中为Insect)即会被执行,然后是下一个导出类,以此类推。这种方式很重要,因为导出类的静态初始化可能会依赖于基类成员能否被正确初始化的。
至此为止,必要的类都已加载完毕(静态变量和静态块),对象就可以被创建了。
首先,对象中所有的原始类型都会被设为缺省值,对象引用被设为null——这是通过将对象内存设为二进制零值而一举生成的。然后,基类的构造器会被调用。在本例中,它是被自动调用的。但你也可以用super 来指定对基类构造器的调用。基类构造器和导出类的构造器一样,以相同的顺序来经历相同的过程,即向上寻找基类构造器。在基类构造器完成之后(即根部构造器找到之后),实例变量(instance variables )按其次序被初始化(注意观察代码中的Tag())。最后,构造器的其余部分被执行。
10.参考资料
Thinking in Java 3rd