经典题目 java类的加载顺序及理解何为java向前引用

来源:互联网 发布:网络教育统考时间报名 编辑:程序博客网 时间:2024/06/04 00:25

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
public class StaticTest {
     
    public static int k = 0;
    public static StaticTest t1 = new StaticTest("t1");
    public static StaticTest t2 = new StaticTest("t2");
    public static int i = print("i");
    public static int n = 99;
    public int j = print("j");
     
    {
        print("构造快");
    }
     
    {
        print("静态块");
    }
     
    public StaticTest(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++n;
        ++i;
    }
     
    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++i;
        return ++n;
    }
    public static void main(String[] args) {
        StaticTest t = new StaticTest("init");
    }
 
}
运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
1:j i=0 n=0
2:构造快 i=1 n=1
3:静态块 i=2 n=2
4:t1 i=3 n=3
5:j i=4 n=4
6:构造快 i=5 n=5
7:静态块 i=6 n=6
8:t2 i=7 n=7
9:i i=8 n=8
10:j i=9 n=99
11:构造快 i=10 n=100
12:静态块 i=11 n=101
13:init i=12 n=102

理解:

首先加载的顺序为:
先父类的static成员变量-》子类的static成员变量-》父类的成员变量-》父类构造-》子类成员变量-》子类构造
也就是说最先被加载的是所有static申明的成员变量,之所以被申明为静态,特点就是共享,即使实例化多个对象,但是是共用一个static声明的变量的。
也就是说,首先所有的static被载入,但是还未执行,下一步开始执行,自上而下,首先执行完第一行之后执行public static StaticTest t1 = new StaticTest("t1"); 
实例化这个对象的时候,由于静态的已经被载入,所以就直接执行
public int j = print("j"); 这一句,然后执行
{         print("构造快");     }           {         print("静态块");     } 
最后执行构造函数,
然后实例化t2,
最后实例化对象。

做几个例子测试出该效果,推出什么原理大家自己理解吧。
第一个,public static StaticTest t1 = new StaticTest("t1");改为
public StaticTest t1 = new StaticTest("t1");  
结果:加载出错
第二个,把public int j = print("j");也改为静态的。


类加载顺序:
 *  1.加载类的静态属性(非静态不管)
 *  这里加载的是:public static int k = 0;
 *  然后加载:public static StaticTest t1 = new StaticTest("t1");
 *  因为此处进行了类的实例化所以
 *  1.1加载类的非静态属性
 *  这里是:public int j = print("j");
 *  运行完这个方法接着
 *  1.2顺序加载类中的非static代码块(static暂时不加载)
 *  这里是:print("构造快");和print("静态块");
 *  运行完接着
 *  1.3加载类的构造方法
 *  这里是:public StaticTest(String str)
 *  运行完(一个静态属性的实例就完成了)
 *  2.继续加载类的静态属性
 *  这里加载的是:public static StaticTest t2 = new StaticTest("t2");
 *  2.1重复(1.1-1.3)
 *  3.继续加载类的静态属性
 *  这里加载的是:public static int i = print("i");
 *  运行完接着
 *  4.继续加载类的静态属性
 *  这里加载的是:public static int n = 99;
 *  不管你n原来是多少现在为99
 *  接着
 *  5.(如果有static代码块,在这里先加载,这个里面没有所以加载主函数)加载主函数
 *  这里加载的是:StaticTest t = new StaticTest("init");
 *  因为此处进行了类的实例化所以
 *  5.1
 *  重复1.1-1.3
 *  5.2
 *  因为public static int print(String str)这个方法返回++n
 *  所以n从99开始累加
 *  运行完OK了

疑问:

static被载入时,int值应该为空吧.
运行到创建t1的实例时,还没有运行static i的初始化代码吧? 
为什么创建t1实例的时候,可以直接做i++?

还有一个知识点就是关于向前引用的问题,像结果中n开始为什么会0,后面有变为99。这个可以参考一下:

所谓向前引用,就是在定义类、接口、方法、变量之前使用它们,例如,

1
2
3
4
5
6
7
8
class MyClass
{
    void method()
    {
        System.out.println(myvar);
    }
    String myvar = "var value";
}

      myvar在method方法后定义,但method方法可以先使用该变量。在很多语言,如C++,是需要提前定义的,而Java已经允许了向前引用。不过在使用向前引用时可能会容易犯一些错误。例如,下面的代码。

1
2
3
4
5
class MyClass {
     int method() {return n; }
     int m = method();
     int n = 1;
}

       如果简单地执行下面的代码,毫无疑问会输出1.

1
System.out.println(new MyClass().method());

       不过使用下面的代码输出变量m,却得到0。

1
System.out.println(new MyClass().m);

    那么这是真么回事呢?

   实际上,从java编译器和runtime的工作原理可以得知。在编译java源代码时只是进行了词法、语法和语义检测,如果都通过,会生成.class文件。不过这时MyClass中的变量并没有被初始化,编译器只是将相应的初始化表达式(method()、1)记录在.class文件中。

   当runtime运行MyClass.class时,首先会进行装载成员字段,而且这种装载是按顺序执行的。并不会因为java支持向前引用,就首先初始化所有可以初始化的值。首先,runtime会先初始化m字段,这时当然会调用method方法,在method方法中利用向前引用技术使用了n。不过这时的n还没有进行初始化呢。runtime为了实现向前引用,在进行初始化所有字段之前,还需要将所有的字段添加到符号表中。以便在任何地方(但需要满足java的调用规则)都可以引用这些字段,不过由于还没有初始化这些字段,所以这时符号表中所有的字段都使用默认的值。int类型的字段默认值自然是0了。所以在初始化int m = method()时,method方法访问的n实际上是在进行正式初始化之前已经被添加到符号表中的字段n,而不是后面的int n = 1执行的结果。但将MyClass改成如下的形式,结果就完全不同了。

1
2
3
4
5
class MyClass {
    int method() {return n; }
    int n = 1;
    int m = method();
}

现在执行下面的代码,会输出1。

1
System.out.println(new MyClass().m);

    究其原因,是引用初始化m时调用method方法,该方法中使用的n已经是初始化完的了,而不是最初放到符号表中的值。

   综合上述,runtime在运行.class文件时,每个作用域(方法、接口、类等带语言元素都有自己的作用域)的符号表都会被至少访问两次,第一次会将所有的字段(这里只考虑类的初始化)放到符号表中,暂时不考虑初始化只,放到符号表中只是相当于一个索引,好让其他地方引用该字段时可以找到它们,例如,method方法中引用n时就会到符号表中寻找n,不过这时的n只是int类型的默认值。等到第二次访问n就是真正初始化n的时候(int n = 1)。这是将符号表中存储的字段n的值更新为实际的初始化值(1)。所以如果引用n放生在正式初始化n之前,当然输出的是0。

   那么可能有人会问,先访问一下n,再访问m,这时m的值是否为1呢?答案仍然是0。因为在创建MyClass对象时m和n的初始化工作已经完成,它们的值已成事实,除非再次设置,否则不可改变了。

1
2
3
MyClass myClass = new MyClass();
System.out.println(myClass.n);  //  输出1
System.out.println(myClass.m);  //  仍然输出0

对于静态成员,仍然符合这一规则。   

1
2
3
4
5
class MyClass {
     static int method() {return n; }
     static int m = method();  //  直接访问m,仍然会输出0
     static int n = 1;
}

本文出自 “李宁的极客世界” 博客,请务必保留此出处http://androidguy.blog.51cto.com/974126/1230298





原创粉丝点击