原题目:Java的一道面试题----静态变量初始化过程
题目如下:
public class Test{ private static Test tester = new Test(); //step 1 private static int count1; //step 2 private static int count2 = 2; //step 3 public Test(){ //step 4 count1++; count2++; System.out.println("" + count1 + count2); } public static Test getTester(){ //step 5 return tester; } public static void main(String[] args){ Test.getTester(); } }
猜猜以上代码执行的顺序,输出的结果
====================================================
提示一下:将step 1和step 3位置换一下结果不同
====================================================
结果是:11
然后,变量最终的取值:count1=1;count2=2;
好了,如果你知道是怎么回事了,刚好不浪费你的时间,如果还是有疑问,可以往下看
====================================================
1.JVM虚拟机启动是通过引导类加载器(Bootstrap Class Loader)加载一个初始化类来完成,这个类由虚拟机的具体实现指定,也就是一般意义上的启动类(主类);然后虚拟机链接这个类,初始化并调用它的 public void main(String[])方法。——简单理解,就是有main函数的会先运行
2,面试题中根据上下文可认为初始化类就是Test类,所以:
a, 首先装载这个类,然后在链接的准备阶段(链接包括验证、准备、引用三个阶段),为所有类(静态)变量分配内存,设为为默认值(Test tester = null; int count1 = 0; int count2 = 0;)
b, 链接完成后,进行(类)初始化,按代码中声明顺序进行类(静态)变量的初始化,也就是先调用
-
private static Test tester =new Test(); //step 1
注:这里省略了基类初始化和<clinit>的相关细节。
c, 上述步骤中的new 触发Test类的实例化(对象创建),先在堆上分配内存,然后设置对象变量(本例中没有)为初始值,然后调用<init>,细节略过,简单来讲这里会导致构造方法的调用,也就是:
- Java code
-
public Test(){ //step 4 count1++; count2++; System.out.println(""+ count1 + count2); }
很显然,这时候的count1和count2并没有被初始化,只是简单的被设置为默认值0(在链接的准备阶段)。所以打印出来的值总是11。
——可以简单理解,对于tester,count1,count2先声明,都初始化为默认值,然后顺序执行,发现tester在声明的时候实例化了,而对一个Test()类实例化的时候则会调用其构造函数。而当执行构造函数的时候,count1和count2还是初始化的值,即都为0。所以最终输出结果为11,但继续执行的时候,count2又会被赋值为2。
也就是说,真正的执行顺序是14235。
=====================================================
回过头想想,Java中,一个类的构造函数只有在这个类实例化的时候才会调用,在这个类实例化的时候,才会分配空间。而我们发现,执行main的时候,也是没有构造函数的,所以主类自己也并没有实例化,也就是说主类也是一个虚的引用。所以我将上面静态标识为红色,否则报错——静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在
不自然的就喜欢和C++联系一下
以下是《C++prime》中有一些对类的声明和定义的区分
一旦到了类体的结尾即结束右括号我们就说一个类被定义了一次一旦定义了一个 类则该类的所有成员就都是已知的类的大小也是已知的了 我们也可以声明一个类但是井不定义它例如 class Screen; // Screen 类的声明 这个声明向程序引入了一个名字Screen 指示Screen 为一个类类型 但是我们只能以有限的方式使用已经被声明但还没有被定义的类类型如果没有定义类 那么我们就不能定义这类类型的对象因为类类型的大小不知道编译器不知道为这种类类 型的对象预留多少存储空间 但是我们可以声明指向该类类型的指针或引用允许指针和引用是因为它们都有固定 的大小这与它们指向的对象的大小无关但是因为该类的大小和类成员都是未知的所 以要等到完全定义了该类我们才能将解引用操作符* 应用在这样的指针上或者使用指 针或引用来指向某一个类成员 只有已经看到了一个类的定义我们才能把一个数据成员声明成该类的对象在程序文 本中还没有看到该类定义的地方数据成员只能是该类类型的指针或引用
这里是让我有点恍然大悟的感觉的地方
因为只有当一个类的类体已经完整时它才被视为已经被定义所以一个类不能有自身 类型的数据成员但是当一个类的类头被看到时它就被视为已经被声明了所以一个类 可以用指向自身类型的指针或引用作为数据成员
由此看来,C++的声明等价于Java中未实例化的类对象
C++的定义等价于Java中的实例化