Java:初始化类、变量、程序块加载探讨

整理了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

你可能感兴趣的:(java,加载,初始化)