java 初始化问题

http://wxl24life.iteye.com/blog/1924719


在 Java 里定义一个类的时候,很多时候我们需要提供成员变量,成员变量专业叫法是 Memeber Variable 或者干脆的叫作 Field. 根据是否使用 static 关键字修饰,可以将 Field 分为两种:

  1. static field:也称作 class variable,这种 filed 属于 class,并不属于单个 instance,所有该 class 的 intance 共享内存中的同一份 class field。
  2. non-static field:也称作 instance variable,它属于每一个具体的 instance,class 的每一个 instance 独享一份 non-static field。

接下来进入本文的主题:java 中 field 的初始化方式。

从初始化代码所在的位置看,可以粗略的分为三种:

  1. 在声明 field 的地方进行初始化。
  2. 在类的构造方法(constructor) 里对 field 进行初始化
  3. 在初始化块(initialization block) 中对已声明的 field 进行初始化

第一种方式主要用于简单的赋值,使用这种方式的前提是作为初始化变量的值是已知的并且通常可以使用单行的赋值语句完成(例外?参见 Double Brace Initialization)。

 

Java代码   收藏代码
  1. public class Foo {  
  2.     // class variable initializer  
  3.     private static Logger logger = LoggerFactory.getLogger(Foo.class);  
  4.   
  5.     // instance variable initializer  
  6.     private List<String> fooList = new ArrayList<String>();  
  7. }  

 

对于复杂的初始化语句,如包含异常处理(try-catch)、使用循环结构初始化等,则需要考虑另外两种初始化方式:constructor 和 initialization block。其中 initialization block 根据是否由 static 关键字修饰,又可分为 static(class) initialization block 和 instance(object) initialization block,前一种只能初始化 class variable,用它进行 instance variable 的初始化会导致编译错误。

 

构造方法(constructor)可以用于初始化 instance variable。除此之外,少数情况下,instance variable 的初始化需要考虑使用 instance initialization block 完成。例如,在匿名类中的初始化(因匿名类无构造方法),或者类中包含了多个 constructor,而它们有公共的一些复杂初始化操作,此时可以考虑将这些操作提取到 instance initialization block 里。除了这两种情况,在你的代码中应该尽量少使用 instance initialization block。

 

static initialization block 用于处理带有 class variable 的初始化操作。

 

Java代码   收藏代码
  1. public class BarClass {  
  2.   
  3.     private static Properties propTable;  
  4.   
  5.     static {  
  6.         try {  
  7.             propTable.load(new FileInputStream("/data/user.prop"));  
  8.         } catch (Exception e) {  
  9.             propTable.put("user", System.getProperty("user"));  
  10.             propTable.put("password", System.getProperty("password"));  
  11.         }  
  12.     }  
  13. }  

 

static initialization block  的另一个功能与线程安全(thread safe)相关。JVM 保证使用同一个 ClassLoader 加载的类中的 static initialization block 只被执行一次,因而它是线程安全的。也正因为这一点,很多时候我们可以利用 static initialization block 执行一些初始化(write)操作,而无需对该 block 使用任何同步机制。

 

最后来看一下初始化代码的执行顺序问题。在此之前,先看下面这段代码,它可以完整执行,请试着分析一下最后的输出是什么。

 

Java代码   收藏代码
  1. /** 
  2.  * @author wxl24life 
  3.  * 
  4.  */  
  5. public class ClassInitializerOrderTest{  
  6.     public static void main(String[] args) {  
  7.         B a = new B();  
  8.     }  
  9. }  
  10.   
  11. class A {  
  12.     static int a = initA();  
  13.     static int initA() {  
  14.         System.out.println("call 1");  
  15.         return 0;  
  16.     }  
  17.       
  18.     {  
  19.         System.out.println("call 5");  
  20.     }  
  21.     {  
  22.         System.out.println("call 6");  
  23.     }  
  24.       
  25.     static {  
  26.         System.out.println("call 2");  
  27.     }  
  28.     static {  
  29.         System.out.println("call 3");  
  30.     }  
  31.       
  32.     static int b = initB();  
  33.     static int initB() {  
  34.         System.out.println("call 4");  
  35.         return 0;  
  36.     }  
  37.   
  38.     A() {  
  39.         System.out.println("call 7");  
  40.     }  
  41. }  
  42.   
  43. class B extends A {  
  44.     {  
  45.         System.out.println("call 8");  
  46.     }  
  47.   
  48.     String str = initStr();  
  49.     String initStr() {  
  50.         System.out.println("call 9");  
  51.         return "call 8";  
  52.     }  
  53.       
  54.     static {  
  55.         System.out.println("call 10");  
  56.     }  
  57.   
  58.     B() {  
  59.         super();  
  60.         System.out.println("call 12");  
  61.     }  
  62.   
  63.     {  
  64.         System.out.println("call 11");  
  65.     }  
  66. }  

 

用几句话概括下初始化顺序规则(假设调用方式类似于上面代码,即使用 new 操作符 ):

  1. static 先于 non-static, non-static 先于 constructor。这里的 static 统指 static field 和 static initialization block 两种初始化方式,non-static 同上。
  2. static 初始化代码按照在源代码中定义的顺序从上往下以此执行,non-static 同上。
  3. 存在继承关系时,优先执行基类中的初始化语句。

执行顺序测试代码的输出结果:

Console text代码   收藏代码
  1. call 1  
  2. call 2  
  3. call 3  
  4. call 4  
  5. call 10  
  6. call 5  
  7. call 6  
  8. call 7  
  9. call 8  
  10. call 9  
  11. call 11  
  12. call 12  

 

参考阅读:


http://coolshell.cn/articles/5826.html

让我们先来看两个类:Base和Derived类。注意其中的whenAmISet成员变量,和方法preProcess()

1
2
3
4
5
6
7
8
public class Base
{
     Base() {
         preProcess();
     }
 
     void preProcess() {}
}
1
2
3
4
5
6
7
8
9
public class Derived extends Base
{
     public String whenAmISet = "set when declared" ;
 
     @Override void preProcess()
     {
         whenAmISet = "set in preProcess()" ;
     }
}

如果我们构造一个子类实例,那么,whenAmISet 的值会是什么呢?

1
2
3
4
5
6
7
8
public class Main
{
     public static void main(String[] args)
     {
         Derived d = new Derived();
         System.out.println( d.whenAmISet );
     }
}

再续继往下阅读之前,请先给自己一些时间想一下上面的这段程序的输出是什么?是的,这看起来的确相当简单,甚至不需要编译和运行上面的代码,我们也应该知道其答案,那么,你觉得你知道答案吗?你确定你的答案正确吗?

很多人都会觉得那段程序的输出应该是“set in preProcess()”,这是因为当子类Derived 的构造函数被调用时,其会隐晦地调用其基类Base的构造函数(通过super()函数),于是基类Base的构造函数会调用preProcess() 函数,因为这个类的实例是Derived的,而且在子类Derived中对这个函数使用了override关键字,所以,实际上调用到的是:Derived.preProcess(),而这个方法设置了whenAmISet 成员变量的值为:“set in preProcess()”。

当然,上面的结论是错误的。如果你编译并运行这个程序,你会发现,程序实际输出的是“set when declared ”。怎么为这样呢?难道是基类Base 的preProcess() 方法被调用啦?也不是!你可以在基类的preProcess中输出点什么看看,你会发现程序运行时,Base.preProcess()并没有被调用到(不然这对于Java所有的应用程序将会是一个极具灾难性的Bug)。

虽然上面的结论是错误的,但推导过程是合理的,只是不完整,下面是整个运行的流程:

  1. 进入Derived 构造函数。
  2. Derived 成员变量的内存被分配。
  3. Base 构造函数被隐含调用。
  4. Base 构造函数调用preProcess()。
  5. Derived 的preProcess 设置whenAmISet 值为 “set in preProcess()”。
  6. Derived 的成员变量初始化被调用。
  7. 执行Derived 构造函数体。

等一等,这怎么可能?在第6步,Derived 成员的初始化居然在 preProcess() 调用之后?是的,正是这样,我们不能让成员变量的声明和初始化变成一个原子操作,虽然在Java中我们可以把其写在一起,让其看上去像是声明和初始化一体。但这只是假象,我们的错误就在于我们把Java中的声明和初始化看成了一体在C++的世界中,C++并不支持成员变量在声明的时候进行初始化,其需要你在构造函数中显式的初始化其成员变量的值,看起来很土,但其实C++用心良苦。

在面向对象的世界中,因为程序以对象的形式出现,导致了我们对程序执行的顺序雾里看花。所以,在面向对象的世界中,程序执行的顺序相当的重要

下面是对上面各个步骤的逐条解释。

  1. 进入构造函数。
  2. 为成员变量分配内存。
  3. 除非你显式地调用super(),否则Java 会在子类的构造函数最前面偷偷地插入super() 。
  4. 调用父类构造函数。
  5. 调用preProcess,因为被子类override,所以调用的是子类的。
  6. 于是,初始化发生在了preProcess()之后。这是因为,Java需要保证父类的初始化早于子类的成员初始化,否则,在子类中使用父类的成员变量就会出现问题。
  7. 正式执行子类的构造函数(当然这是一个空函数,虽然我们没有声明)。

你可以查看《Java语言的规格说明书》中的 相关章节 来了解更多的Java创建对象时的细节。

C++的程序员应该都知道,在C++的世界中在“构造函数中调用虚函数”是不行的,Effective C++ 条款9:Never call virtual functions during construction or destruction,Scott Meyers已经解释得很详细了。

在语言设计的时候,“在构造函数中调用虚函数”是个两难的问题。

  1. 如果调用的是父类的函数的话,这个有点违反虚函数的定义。
  2. 如果调用的是子类的函数的话,这可能产生问题的:因为在构造子类对象的时候,首先调用父类的构造函数,而这时候如果去调用子类的函数,由于子类还没有构造完成,子类的成员尚未初始化,这么做显然是不安全的。

C++选择了第一种,而Java选择了第二种。

  • C++类的设计相对比较简陋,通过虚函数表来实现,缺少类的元信息。
  • 而Java类的则显得比较完整,有super指针来导航到父类。

最后,需要向大家推荐一本书,Joshua Bloch 和 Neal Gafter 写的 Java Puzzlers: Traps, Pitfalls, and Corner Cases,中文版《JAVA解惑》。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值