think in java之构造器的真正调用顺序

原文链接地址:http://blog.csdn.net/yaerfeng/article/details/7294882 

 

构造器是OOP的重要组成部分,很多人认为它很容易。只不过是new了一个对象而已。而think in java的作者却告诉我们,其实这并不容易。

先看下面这个例子。在你没看结果之前,你觉得你的答案是对的么。

 

  
  
  
  
  1. package com.tudou.t1;   
  2.    
  3. class Meal {   
  4.     Meal() {   
  5.         System.out.println("meal");   
  6.     }   
  7. }   
  8.    
  9. class Bread {   
  10.     Bread() {   
  11.         System.out.println("Bread");   
  12.     }   
  13. }   
  14.    
  15. class Cheese {   
  16.     Cheese() {   
  17.         System.out.println("Cheese");   
  18.     }   
  19. }   
  20.    
  21. class Lettuce {   
  22.     Lettuce() {   
  23.         System.out.println("Lettuce");   
  24.     }   
  25. }   
  26.    
  27. class Lunch extends Meal{   
  28.     Lunch() {   
  29.         System.out.println("Lunch");   
  30.     }   
  31. }   
  32.    
  33. class PortableLunch extends Lunch{   
  34.     PortableLunch() {   
  35.         System.out.println("PortableLunch");   
  36.     }   
  37. }   
  38.    
  39. public class Sandwich extends PortableLunch {   
  40.     private Bread b = new Bread();   
  41.     private Cheese c = new Cheese();   
  42.     private Lettuce l = new Lettuce();   
  43.    
  44.     public Sandwich() {   
  45.         System.out.println("Sandwich");   
  46.     }   
  47.    
  48.     public static void main(String[] args) {   
  49.         new Sandwich();   
  50.     }   
  51. }   

控制台的打印结果为:

meal
Lunch
PortableLunch
Bread
Cheese
Lettuce
Sandwich

 

复杂对象调用构造器的顺序应该遵循下面的原则:
1,调用基类[即父类]构造器。这个步骤会不断反复递归下去,首先是构造器这种层次结构的根,然后是下一层导出类[即子类],等等。直到最底层的导出类。[从最上层的meal一直递归到PortableLunch]

2,按声明顺序调用成员的初始化方法。[即上面的Bread,Cheese,Lettuce]

3,调用导出类构造器的主体[即Sandwich]

可见,调用类本身是最后完成初始化的,最先完成初始化的是最顶级的基类,所谓没有父亲,哪来的儿子。处于它们中间的是调用类本身拥有的子对象。因为你不可能在子对象初始化之前用本类调用它,所以它一定在本类调用之前,父类调用之后完成初始化的。

 

那么这个说法是不是一定成立呢。结果是否定的。你必须知道JVM的编绎原理才可能知道,它究竟是如何工作的。

我们来看下面这个例子,来解释为什么它不一定。因为在继承和重写的时候,这种情况变得有点诡异。

深入探究:

 

  
  
  
  
  1. package com.tudou.t1;   
  2.    
  3. public class ConstrcutorTest2 {   
  4.     public static void main(String[] args) {   
  5.         new RoundGlyph(5);   
  6.     }   
  7. }   
  8.    
  9. class Glyph {   
  10.    
  11.     void draw() {   
  12.         System.out.println("Glyph draw()");   
  13.     }   
  14.    
  15.     Glyph() {   
  16.         System.out.println("Glyph before draw();");   
  17.         draw();   
  18.         System.out.println("Glyph after draw();");   
  19.     }   
  20. }   
  21.    
  22. class RoundGlyph extends Glyph {   
  23.     private int radius = 1;   
  24.    
  25.     RoundGlyph(int r) {   
  26.         radius = r;   
  27.         System.out.println("RoundGlyph(),radius:" + radius);   
  28.     }   
  29.    
  30.     void draw() {   
  31.         System.out.println("RoundGlyph.draw(),radius:" + radius);//此处打印是0,而不是1   
  32.     }   
  33. }   

控制台打印结果:

Glyph before draw();
RoundGlyph.draw(),radius:0
Glyph after draw();
RoundGlyph(),radius:5

为什么RoundGlyph.draw(),radius:0这里会是0呢。

默认的1哪去了?值自己会变么。其实上面的讲述并不完整。,而这正是解决谜题的关键所在。初始化的实际过程之前,实际在还有一步。

 

0:在其他任何事物发生之前,将分配对象的存舍得空间初始化为二进制的零。

而它后面的初始化顺序就是上面的3步。

1,调用基类[即父类]构造器。这个步骤会不断反复递归下去,首先是构造器这种层次结构的根,然后是下一层导出类[即子类],等等。直到最底层的导出类。

2,按声明顺序调用成员的初始化方法。

3,调用导出类构造器的主体

 

 

也就是说,实际上有4步,知道这些你对对象初始化构造器才可能有个清楚的认识。

JAVA有更多的精髓等着人们去挖掘,而不仅仅是知道如何去使用它。

因为你不知道什么时候它会出现意想不到的后果,而这个错误,可能你根本就想不出来。

 

编写构造器时有一条准则:

用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其它方法。

 

在构造器内唯一能够安全调用的那些方法是基类中的final或者private方法,这些方法不能被覆盖,因此也就不会出现令人惊讶的问题。

你可能无法总是遵循这条准则,但是应该朝着它努力。

 

学任何语言,请打好基础,它是你以后扩展的人生基石。

原文链接地址:http://blog.csdn.net/yaerfeng/article/details/7294882 

你可能感兴趣的:(构造器)