深度思考Java成员变量的初始化

来源:互联网 发布:淘宝买片搜什么关键词 编辑:程序博客网 时间:2024/04/30 11:31

      上次同学问我Java的初始化问题时,我只是简单的告诉了他答案,今天参考CSDN博主 luohuacanyue (博客地址见参考)写的 深度思考Java成员变量的初始化 ,谈谈自己对这个的理解。

      Java代码的时候很少去关注成员变量的声明和初始化顺序,今天借此机会抛出一些问题:语言的设计者们为什么会这样设计?比如说很常见的一个问题:abstract(抽象)类不能用final进行修饰。这个问题比较好理解:因为一个类一旦被修饰成了final,那么意味着这个类是不能被继承的,而abstract(抽象)类又不能被实例化。如果一个抽象类可以是final类型的,那么这个类又不能被继承也不能被实例化,就没有存在的意义。从语言的角度来讲一个类既然是抽象类,那么它就是为了继承,所以给它标识为final是没有意义的。语言的设计者们当然不可能让这么大的一个bug产生。对于开发者而言抽象类不能修饰final可能就是一种约定俗成的规定,并没有特殊意义。我们完全可以往前想一点:为什么这么设计?

       下面我所展示的一些代码实例也同样会采用我上面的一些思考方法。有一些是一些”契约“,并没有特别的缘由,可能用别的方法也是合理的。下面的代码会讲到初始化的一些策略,从实际的执行结果中得出一些结论

代码一、

[java] view plaincopy
  1. public class Test1 {  
  2.     {  
  3.         a = 1;  
  4.         //System.out.println(a);//这里会抛错,但是把这个初始化块移动到 private int a=2;之后又不会报错。
  5.     }  
  6.     private int a=2;//这里初始化一次,上面的动态块中也对a进行了赋值,这个时候a=?,为什么可以对a进行赋值,而不可以对a进行输出  
  7.     public static void main(String[] args){  
  8.         Test1 test1 = new Test1();  
  9.         System.out.println(test1.a);  
  10.     }  
  11. }  

看看上面的代码一,第一个问题就是这段代码能否编译通过。结果是能编译通过。这里说明一个问题就是变量的声明和赋值是两步操作(注意此处是声明在调用之后,调用在声明之后是可以的运行的,是否我们可以理解我Java语言设计者觉得作者在使用变量前,一定要确定自己的行为,所以让使用者一定要在声明后才允许使用?)第一个问题解决了。那下一个问题很显然最后输出的结果是什么?答案是“2”,这里可能会有些诧异。从直观上来讲就是说明在赋值的过程中是完全按照代码的前后顺序进行。

代码二

[java] view plaincopy
  1. public class Test2 {  
  2.       
  3.     {  
  4.         a = 4;  
  5.     }  
  6.       
  7.     private final int a;//这里我并没有对a做初始化。  
  8.       
  9.     public static void main(String[] args){  
  10.         Test2 test2 = new Test2();  
  11.         System.out.println(test2.a);  
  12.     }  
  13. }  

       “代码二”只是在“代码一”的基础上对成员变量a多修饰了一个final,另外我并没有立即初始化。第一个问题就是这段代码能不能编译通过,答案是能。在这里展示这段代码是为了后面做铺垫,因为这段代码仍然符合上面的“契约”

代码三

[java] view plaincopy
  1. public class Test3 {  
  2.       
  3.     {  
  4.         a = 4;  
  5.     }  
  6.       
  7.     private static int a;  
  8.       
  9.     public static void main(String[] args){  
  10.         Test3 test3 = new Test3();//注意:这里开始new了一个对象  
  11.         System.out.println(test3.a);  
  12.     }  
  13. }  

       代码三在代码一的的基础上对于成员变量a多修饰了一个static。这里同样可以编译通过,最后输出的结果也皆大欢喜为4。这里要注意的是我是new了一个对象,而不是直接访问静态变量。(意味着,最后调用了动态初始化块,使得a 由初始化a = 0,转化为 a = 4)

代码四

[java] view plaincopy
  1. public class Test3 {  
  2.     {  
  3.         a = 4;  
  4.         System.out.println(a);//这里不会报错,但是这条语句并不会执行,究其原因没有创造动态对象,只是简单的对类的静态变量进行初始化。 
  5.     }  
  6.     private static int a;  
  7.       
  8.     public static void main(String[] args){  
  9.         System.out.println(a);  
  10.     }  
  11. }  

        代码四在代码三的基础上把new 对象给去掉了,直接输出静态变量a。这时候就会出现非常诧异的结果0。对,你没有看错,结果是0。如果有兴趣的可以在a=4后面打印一条,会很清晰的发现并没有执行a=4那一条语句。这里先不解释,只看一下现象。

代码五 

[java] view plaincopy
  1. public class Test3 {  
  2.     static{  
  3.         a = 4;  
  4.         //System.out.println(a);//这里会抛错。同代码一
  5.     }  
  6.     private static int a;  
  7.     public static void main(String[] args){  
  8.         System.out.println(a);  
  9.     }  
  10. }  

        代码五和代码四和不同在于采用了静态初始化,最后的结论很简单,结果为4。这里有一个问题就是如果在a=4之后紧接着使用a就会报错。也就是说定义在声明之前的静态化块只能对声明变量进行赋值,并不能使用该变量

代码六

[java] view plaincopy
  1. public class Test6 {  
  2.       
  3.     {  
  4.         a = 4;  
  5.     }  
  6.       
  7.     private static final int a;  
  8.       
  9.     public static void main(String[] args){  
  10.         System.out.println(a);  
  11.     }  
  12. }  
       代码六是在代码一的基础上增加static final的修饰符。回到我们上面三段代码所问的问题,这次的答案是“否”,也就是说在这里是不能编译通过。在这里我估计有一部分人和我有同样的疑惑:为什么对于成员变量修饰单独修饰final或者static可以进行单独的初始化,而把两个修饰符合起来的时候就不行了呢?我们把这个问题要反过来问:如果可以这样进行初始化会产生什么问题,那么就可以知道为什么需要这样设计

       我们看代码三、代码四、代码五和代码六,这里估计会有点绕。上面也说了在代码三中的初始化块是执行了的,而代码四的初始化块没有执行,代码五的静态初始化块也执行了。所以问题归根结底就一条:静态初始化块和普通的初始化块在什么时候执行。结论就是在初始化阶段编译器会收集类中的类变量(区别实例变量)的赋值动作和静态语句块中的语句,而静态的调用并不会触发实例变量的初始化

       这里回到代码六,根据上面所得出的结论。变量a被修鉓成了static final,那么意味着有且仅有一次赋值。我们在访问a的同时,域中的a=4并未执行(根据代码四所得出的结论),这样就违背了final类型有且仅有一次赋值的这样一个约定。所以{a=4;}不管是放在声明的代码前还是声明的代码后都无法编译通过。

代码七

[java] view plaincopy
  1. public class Test7 {  
  2.       
  3.     static{  
  4.         a = 5;  
  5.     }  
  6.       
  7.     private static final int a;  
  8.       
  9.       
  10.     public static void main(String[] args){  
  11.         System.out.println(a);  
  12.     }  
  13. }  

      代码七和代码六的不同在于使用静态化的初始化方法,并不会违背final有且仅有一次赋值这样的一个约定。

总结

       1、定义在声明之前的静态化块只能对声明变量进行赋值,并不能使用该变量。

       2、静态初始化时,类先被加载到内存,先为静态成员变量初始化,初始化顺序之上而下。

       3声明和初始化是两个不同的操作,声明在编译时已经完成。分配好空间后开始至上而下静态域赋值。

       4综上,那么我们的初始化顺序应该是 静态初始化器(至上而下赋值(包括直接赋值仍然符合至上而下))-动态初始化器 - 构造器初始化。

       5初始化父类和子类时顺序应该是: 父类静态初始化器(至上而下赋值(包括直接赋值仍然符合至上而下))子类静态初始化器-父类动态初始化器 - 父类构造器初始化-子类动态初始化器 -子类构造器初始化。

参考1: http://blog.csdn.net/luohuacanyue/article/details/13169173

参考2: http://blog.csdn.net/darxin/article/details/5293427

0 0
原创粉丝点击