java 构造函数 成员函数初始化顺序 以及多态的构造函数的调用顺序

by admin on 2020年1月28日

        最近闲着的时候在复习《Thinking in
Java》,昨天看到了多态这一章,想说说关于Java的构造方法和类的初始化问题。
先来看一个简单的程序:

对于JAVA中类的初始化是一个很基础的问题,其中的一些问题也是易被学习者所忽略。当在编写代码的时候碰到时,常被这些问题引发的错误,感觉莫名其妙。而且现在许多大公司的面试题,对于这方面的考查也是屡试不爽。不管基于什么原因,我认为,对于java类中的初始化问题,有必要深入的了解。Java类的初始化,其实就是它在JVM的初始化问题(类加载的问题),对于它在JVM中的初始化是一个相当复杂的问题,是给专家们来探讨的,所以在这里我只是对一些容易忽略的问题,发表一下个人观点: 
1,在一个类的内部(不考虑它是另一个类的派生类):很多人认为,类的成员变量是在构造方法调用之后再初始化的,先不考虑这种观点的正确性,先看一下下面的代码:

       
这篇跟昨天的那篇《构造方法的调用顺序》有联系。我们知道,构造方法的调用顺序以及类的初始化顺序,是先父类的构造方法,然后初始化子类的成员变量,最后才是子类的构造方法。不过里面其中会有些特殊情况让你的程序看起来发生错误。先看下面的一段程序:

public

[java] view
plaincopy

public

class A extends F5 {

  1. class Test01…{  
  2.     public Test01(int i)…{  
  3.         System.out.println(“Test01 of constractor : ” + i);  
  4.     }  
  5. }  
  6. public class Test02 …{  
  7.     private Test01 t1 = new Test01(1);  
  8.     private int n = 10;  
  9.       
  10.     public Test02()…{  
  11.         System.out.println(“Test02 of constructor : ” + n);  
  12.     }  
  13.     private Test01 t2 = new Test01(2);  
  14.     public static void main(String[] args) …{  
  15.         Test02 test = new Test02();  
  16.     }  
  17.   
  18. }  
  19. 输出的结果为:  
  20. Test01 of constractor : 1  
  21. Test01 of constractor : 2  
  22. Test02 of constructor : 10  

class A extends Father {

private F1 f1 = new F1();

通过输出,可见当生成Test02的实例test时,它并不是首先调用其构造方法而是先是成员变量的初始化,而且成员的初始化的顺序以成员变量的定义顺序有关,先定义的先初始化,初始化后再调用构造方法。其实成员变量的初始化,在类的所有方法调用之前进行,包括构造方法 当类中有Static 修饰的成员呢?测试下面一段代码:

public int i = 1;

private F2 f2 = new F2();

[java] view
plaincopy

A() {

A() {

  1. public class Test03 …{  
  2.     private int i1 = printCommon();  
  3.     private static int i2 = printStatic();  
  4.       
  5.     public Test03()…{  
  6.           
  7.     }  
  8.     public static int printCommon()…{  
  9.         System.out.println(“i1 is init!”);  
  10.         return 1;  
  11.     }  
  12.     public static int printStatic()…{  
  13.         System.out.println(“i2 is init!”);  
  14.         return 2;  
  15.     }  
  16.     public static void main(String[] args) …{  
  17.         Test03 t = new Test03();  
  18.     }  
  19. }  
  20.   
  21. 输出结果为:  
  22. i2 is init!  
  23. i1 is init!  

//i = 5;

System.

可见static的成员比普通的成员变量先初始化。 我们都知道,如果一个类的成员变量没有在定义时,系统会给予系统默认的值,有=号的就直接给予右值,系统在给予初值和=号给予值这2中方式,在执行时间上有先后吗?为了测试,我编写了如下代码:

System.

out.println(“A”);

[java] view
plaincopy

out.println(“i=” + i);

}

  1. public class Test04 …{  
  2.     private static Test04 t1 = new Test04();  
  3.     private static int i1;  
  4.     private static int i2 = 2;  
  5.       
  6.     public Test04()…{  
  7.         i1++;  
  8.         i2++;  
  9.     }  
  10.       
  11.     public static void main(String[] args) …{  
  12.         Test04 t2 = new Test04();  
  13.         System.out.println(“t2.i1 = ” + t2.i1);  
  14.         System.out.println(“t2.i2 = ” + t2.i2);  
  15.     }  
  16. }  
  17. 我们先预计一下输出,可能有几种答案:2和3,3和3,2和2  
  18. 执行代码后:  
  19. t2.i1 = 2  
  20. t2.i2 = 3  

}

public static void main(String args[]) {

为什么是2和3呢?其实代码的执行顺序是这样的:首先执行给t1,i1,i2分别给予初始值null,0,0,再执行
Test04 t1 =new
Test04(),这样i1++,i2++被执行,i1,i2都变为1,执行完毕后接着执行int i1;
i1,i2的值仍然是1,1,当执行int i2 = 2时i2被赋予了值,即i1 =
1,i2=2;再执行Test04 t2 = new Test04(),i1,i2再执行++,此时i1 =2,i2
=3,输出i1,i2,结果就是:t2.i1 = 2,t2.i2 = 3。
通过上面的代码我们可以认为系统默认值的给予比通过等号的赋予先执行。
2,一个类还有上层的类,即父类:
     
当生成一个子类时,大家到知道会调用父类的构造方法。如果子类和父类中都有Static的成员变量呢,其实我们在深入分析一个类的内部初始化后,对于存在父类的类的初始化其实原理都一样,具体以下面的代码为例:

public void p() {

A a =

**[java] view
plaincopy**

System.

new A();

  1. class SuperClass …{  
  2.     static…{  
  3.         System.out.println(“SuperClass of static block”);  
  4.     }  
  5.       
  6.     public SuperClass()…{  
  7.         System.out.println(“SuperClass of constracutor”);  
  8.     }  
  9. }  
  10.   
  11. public class SubClass extends SuperClass…{  
  12.     static…{  
  13.         System.out.println(“SubClass of static block”);  
  14.     }  
  15.       
  16.     public SubClass()…{  
  17.         System.out.println(“SubClass of constracutor”);  
  18.     }  
  19.       
  20.     public static void main(String[] args)…{  
  21.         SuperClass t = new SubClass();  
  22.     }  
  23. }  
  24. 输出结果:  
  25. SuperClass of static block  
  26. SubClass of static block  
  27. SuperClass of constracutor  
  28. SubClass of constracutor  

out.println(“i=” + i);

}

可见当父类,和子类有Static时,先初始化Static,再初始化子类的Static,再初始化父类的其他成员变量->父类构造方法->子类其他成员变量->子类的构造方法。
父类上层还有父类时,总是先执行最顶层父类的Static-->派生类Static-->派生类Static-->…….-->子类Static-->顶层父类的其他成员变量-->父类构造方法-->
派生类的其他成员变量 --> 派生类构造方法-->
……………-->子类其他成员变量-->子类构造方法
讨论到继承,就不得提一下多态:
如果父类构造方法的代码中有子类中被重写得方法,当执行这样的语句
SuperClass super = new SubClass();
初始化时调用父类的构造方法,是执行父类的原方法,还是执行子类中被重写的方法呢?

}

}

[java] view
plaincopy

public static void main(String args[]) {

class

  1. class SuperClass…{  
  2.     public SuperClass()…{  
  3.         System.out.println(“SuperClass of constructor”);  
  4.         m();  
  5.     }  
  6.     public void m()…{  
  7.         System.out.println(“SuperClass.m()”);  
  8.     }  
  9. }  
  10. public class SubClassTest extends SuperClass …{  
  11.     private int i = 10;  
  12.     public SubClassTest()…{  
  13.         System.out.println(“SubClass of constructor”);  
  14.         super.m();  
  15.         m();  
  16.     }  
  17.     public void m()…{  
  18.         System.out.println(“SubClass.m(): i = ” + i);  
  19.     }  
  20.     public static void main(String[] args)…{  
  21.         SuperClass t = new SubClassTest();  
  22.     }  
  23. }  
  24. 可能很多人会认为输出为:  
  25. SuperClass of constructor  
  26. SubClass.m(): i = 10  
  27. SubClass of constructor  
  28. SuperClass.m()  
  29. SubClass.m(): i = 10  
  30. 其实不然!  
  31. 正确输出为:  
  32. SuperClass of constructor  
  33. SubClass.m(): i = 0  
  34. SubClass of constructor  
  35. SuperClass.m()  
  36. SubClass.m(): i = 10  
  37. 在生成对象时,父类调用的M()方法,不是父类的 M()方法,而时子类中被重写了的M()方法!!并且还出现一个怪异的现象,子类的privte  int i 也被父类访问到,这不是和我们说private的成员只能在本类使用的原则相违背了吗?其实我们说的这条原则是编译期间所遵守的,在JAVA程序的编译期间,它只检查语法的合法性,在JAVA的JVM中,即运行期间,不管你声明的什么,对于JVM来说都是透明的,而多态是在运行期间执行的,所以能拿到SubClass的private成员,一点都不奇怪,只是此时还没执行 i = 10,所以在父类的构造方法中调用m()时,系统只能将i赋予系统初值0。  
  38. 下面是我设计的一道完整的初始化例子,可测试你对类的初始化问题是否完整掌握:  
  39. 写出程序运行的结果:  
  40. class A…{  
  41.     private int i = 9;  
  42.     protected static int j;  
  43.     static…{  
  44.         System.out.println(“– Load First SuperClass of static block start!– “);  
  45.         System.out.println(“j = ” + j);  
  46.         System.out.println(“– Load First SuperClass of static block End  — “);  
  47.     }  
  48.       
  49.     public A()…{  
  50.         System.out.println(“——- Load SuperClass of structor start ——–“);  
  51.         System.out.println(“Frist print j = ” + j);  
  52.         j = 10;  
  53.         m();  
  54.         System.out.println(“k = ” + k);  
  55.         System.out.println(“Second print j = ” + j);  
  56.         System.out.println(“———–  Load  SuperClass End    ———– “);  
  57.     }  
  58.       
  59.     private static int k = getInt();  
  60.           
  61.     public static int getInt()…{  
  62.         System.out.println(“Load SuperClass.getInt() “);  
  63.         return 11;  
  64.     }   
  65.     static…{  
  66.         System.out.println(“— Load Second SuperClass of static block!——-“);  
  67.         System.out.println(“j = ” + j);  
  68.         System.out.println(“k = ” + k);  
  69.         System.out.println(“– Load Second SuperClass of static block End — “);  
  70.     }  
  71.       
  72.     public void m()…{  
  73.         System.out.println(“SuperClass.m() , ” + “j = ” +j);  
  74.           
  75.     }  
  76. }  
  77.   
  78. class B extends A …{  
  79.     private  int a = 10;  
  80.       
  81.     static…{  
  82.         System.out.println(“—- Load SubClass of static block!——“);  
  83.         System.out.println(“– Load SubClass of static block End — “);  
  84.     }  
  85.       
  86.     public B()…{  
  87.         System.out.println(“Load SubClass of structor”);  
  88.         m();  
  89.         System.out.println(“—   Load SubClass End  —- “);  
  90.     }  
  91.       
  92.     public void m()…{  
  93.         System.out.println(“SubClass.m() ,” + “a = ” + a );  
  94.     }  
  95. }  
  96.   
  97. public class Test1…{  
  98.     public static void main(String[] args)…{  
  99.         A a = new B();  
  100.     }  
  101. }  
  102. 正确的答案为:  
  103. — Load First SuperClass of static block start!–   
  104. j = 0  
  105. — Load First SuperClass of static block End  —  
  106. Load SuperClass.getInt()  
  107. — Load Second SuperClass of static block!——-  
  108. j = 0  
  109. k = 11  
  110. — Load Second SuperClass of static block End —  
  111. —- Load SubClass of static block!——  
  112. — Load SubClass of static block End —  
  113. ——- Load SuperClass of structor start ——–  
  114. Frist print j = 0  
  115. SubClass.m() ,a = 0  
  116. k = 11  
  117. Second print j = 10  
  118. ———–  Load  SuperClass End    ———–  
  119. Load SubClass of structor  
  120. SubClass.m() ,a = 10  
  121. —   Load SubClass End  —-  

new A();

F1 {

 

}

F1() {

下面需要说明的一点也是至关重要的一点:那就是成员变量的初始化和非static初始化块之间的执行顺序是按照他们出现的先后顺序来执行的

}

System.


class

out.println(“F1”);

**[java] view
plaincopy**

Father {

}

  1. public class Test04  
  2. {      
  3.     //下面的这两行代码放置的顺序,跟执行结果是有关系的  
  4.     private  String t1 = test();  
  5.       
  6.     {  
  7.        System.out.println(“初始化快!”);  
  8.     }  
  9.     //上面的这两行代码放置的顺序,跟执行结果是有关系的  
  10.       
  11.     private String test(){  
  12.         System.out.println(“实例变量的执行过程”);  
  13.         return “test”;  
  14.     }  
  15.   
  16.     public Test04()  
  17.     {  
  18.         System.out.println(“构造方法!”);  
  19.     }  
  20.   
  21.     public static void main(String[] args)  
  22.     {  
  23.         Test04 t2 = new Test04();  
  24.     }  
  25.       
  26. }  

public int i;

}

 

public void p() { }

class

Father() {

F2 {

System.

F2() {

out.println(“Before p()”);

System.

p();

out.println(“F2”);

System.

}

out.println(“After p()”);

}

}

class

}

F3 {

        打印结果是:

F3() {

Before p()

System.

i=0

out.println(“F3”);

After p()

}

i=1

}

       
首先,Father是A的父类。在Father中,有一个成员变量i,一个方法p(),以及Father的构造方法。A中也有一个成员变量i,并且在定义的时候被初始化为1,同样也有一个打印出i的方法p(),以及A的构造方法。
       
程序刚开始运行的时候,从main进入,创建一个A对象。那么,按照昨天讲的,将会先执行Father的构造方法。在Father的构造方法中,先打印出Before
p(),然后调用了子类的方法p()。注意,这个时候,我们看到打印出来的是i=0而不是i=1!我们在写子类代码的时候,明明将i初始化为1了,为什么会出现i=0的情况呢?因为,这个时候父类的构造方法还没有执行完毕,子类中的成员变量并没有执行i=1这个初始化过程,而只有系统给出的默认值(int型的是0,boolean型的是false,对象引用是null等等)。所以我们可以看到结果中是i=0而不是i=1。继续往下执行,打印出After
p()。这时,父类的构造方法执行完毕。然后就是初始化子类的成员变量,即执行i=1。然后再执行子类A的构造方法,打印出了i=1。
        另外,如果我们去掉类A构造方法的注释,我们会看到如下的结果:

class

Before p()

F4 extends F3 {

i=0

F4() {

After p()

System.

i=5

out.println(“F4”);

       
与有注释的结果不同的是最后i=5而不是i=1。这是因为在i被初始化为1之后,我们才执行A的构造方法。而此时构造方法使i=5,然后再打印出i。因此我们看到了i=5。
       
所以,我们可以进一步确定类的初始化与构造方法的调用顺序的关系。这里我们要注意的是,尽量不要在父类的构造方法中调用其他的方法,否则会出现一些意想不到的后果。如果的确需要的话,就调用那些是private或者final的方法,因为这些方法不能被子类继承,不是动态调用的方法。

}

}

class

F5 extends F4 {

F5() {

System.

out.println(“F5”);

}

}
        结果是:

F3

F4

F5

F1

F2

A

        这段代码是怎么执行的呢?首先,F1和F2
是两个单独的类,与其它的类并没有继承关系。从F3类开始,后一个类是前一个类的子类,直到类A。也就是说,按照子类指向父类的顺序,我们可以看出:
        F3<—F4<—F5<—A
        每个类都定义了自己的构造方法,即简单打印出该类的名称。
       
当程序开始运行的时候,main()方法是入口。从main()方法进入后,我们可以看到就只有一条语句,即:
        A a = new A();
       
这句话创建了一个A类的对象。现在开始是重点了。我们知道,当创建一个对象的时候,对象所属类的构造方法将被调用。但是如果这个类是某个类的子类,也就是说,该对象的类有父类的时候,会先去调用父类的构造方法。如果父类还有父类的话,那么就又会上一层调用父类的父类的构造方法,直到最顶层的父类为止(这里Object类不参与讨论,因为这是所有Java类的父类,已经隐藏实现了的),然后再一层一层向下调用子类的构造方法。
       
为什么会先调用父类的构造方法?因为在继承中,子类可以访问父类的public和protected成员。但是这些成员要确保存在才能被子类所访问。因此,创建一个子类的对象的时候,在子类的构造方法里面,必须要确保这些成员被创建,唯一的方法就是先调用父类的构造方法来确保这些成员会被正确创建。这样,进入子类的构造方法的时候,父类的这些成员都已被初始化,能够被我们访问到。
       
现在我们知道为什么会先调用最顶层的父类的调用方法了。回到上面的程序。当执行new
A()的时候,先调用最顶层父类F3的构造方法,然后F4,然后F5。也许有人会问在结果中,F5之后为什么不是A而是F1和F2,然后再是A呢?这里是另一个要注意的,即:当调用完所有父类的构造方法时,将会先初始化子类的成员变量,然后再执行子类的构造方法。A中有两个成员变量,f1和f2,它们分别是F1和F2的对象。因此,在调用A的构造方法前,会先创建f1和f2,然后再执行A的构造方法,所以,我们会在结果中看到F5之后是F1,F2,最后才是A。
        至此,我们可以明白构造方法的调用顺序了:
       
1.有父类的话先调用父类的构造方法,如果父类之上还有父类,则继续向上调用,直到最顶层父类为止,再一层一层向下调用其它父类的构造方法;
        2.当调用完最靠近子类的父类的构造方法时,初始化子类的成员变量;
        3.执行子类的构造方法。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图