Java成员变量的初始化

Java中的成员变量(Member Variable or Field)根据是否有static修饰可分为两类,分别是:

  • static field (class variable): 类所有,所有类的实例在内存中共享一份
  • non-static field (instance variable): 每个类的实例独享一份

Java中通过类来构造实例时,涉及到初始化的部分主要有以下几个方面:

  • 在声明 fields 的地方进行初始化
  • 初始化块(instance and static initializers)中语句的执行
  • 构造函数(constructor)的执行

在声明 field 的位置进行初始化通常用于简单的赋值。对于复杂的初始化语句,如涉及到异常、循环结构赋值等,通常放在初始化块或构造函数中进行。初始化块根据是否使用 static 修饰也可分为静态初始化块和非静态初始化块,在静态初始化块中不能对 instance variable 进行赋值操作。

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

静态初始化块(static initializers)可以对class variable进行初始化,但根据它的一个重要特性和线程安全有关。 JVM 可以保证使用同一个 ClassLoader 加载的类中静态初始化块只被执行一次,因而它是线程安全的。正因为这一点,可以用静态初始化块完成一些特殊的初始化操作而不必考虑对该 block 使用同步机制。

初始化操作的执行顺序归纳如下:

  1. 静态优先于非静态执行(包括成员变量和初始化块)
  2. 静态(成员变量和初始化块)按照源代码中的顺序执行,非静态亦如此
  3. 涉及到继承关系时,优先执行基类中的初始化操作

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class InitTest {
public static void main(String[] args) {
B b = new B();
System.out.println(b.declareAndSet);
}
}

class A {
private static int a1 = a1();
private static int a1() {
System.out.println("A: static int a1 = a1()");
return 0;
}

{
System.out.println("A: non-static initialization block 1");
}

private int a2 = a2();
private int a2() {
System.out.println("A: int a2 = a2()");
return 0;
}

{
System.out.println("A: non-static initialization block 2");
}

A() {
System.out.println("A: A().");
init();
}

public void init() {
System.out.println("A: inti()");
}

static {
System.out.println("A: static initialization block 1");
}
}

class B extends A {
private static int b1 = b1();
private static int b1() {
System.out.println("B: static int b1 = b1()");
return 0;
}

private int b2 = b2();
private int b2() {
System.out.println("B: int b2 = b2()");
return 0;
}

{
System.out.println("B: non-static initialization block 2");
}

public String declareAndSet = "value in declared.";
B() {
System.out.println("B: B()");
}

@Override
public void init() {
System.out.println("B: inti()");
declareAndSet = "value in init()";
}

static {
System.out.println("B: static initialization block 1");
}
}

执行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
A: static int a1 = a1()
A: static initialization block 1
B: static int b1 = b1()
B: static initialization block 1
A: non-static initialization block 1
A: int a2 = a2()
A: non-static initialization block 2
A: A().
B: inti()
B: int b2 = b2()
B: non-static initialization block 2
B: B()
value in declared.

从结果中可以看到,在创建一个B类的实例时,执行的详细步骤如下:

  1. 进入构造函数
  2. 为成员变量分配内存
  3. 执行父类中静态成员变量和静态初始化块相关的操作
  4. 执行子类中静态成员变量和静态初始化块相关的操作
  5. 执行父类中非静态成员变量和非静态初始化块中的相关操作
  6. 执行父类构造函数体中的操作(总是会调用父类构造函数,隐式super()或显示调用。父类构造函数的调用必须是构造器中的第一个语句。)
  7. 执行子类中非静态成员变量和非静态初始化块中的相关操作
  8. 执行子类构造函数主体中的操作

在上面的例子中,我一直错误地以为declareAndSet这个变量的值会是”value in init()”,但实际并非如此。在Java中,成员变量的声明和初始化并不是同时发生的,可以把它看作一个先分配内存再赋值的过程。首先进入A类的构造函数中,调用init()方法;该方法在B类中进行了override,由于类的实例是属于子类B的,因而调用B类中的init()方法,此时declareAndSet=”value in init()”;A的构造函数结束后,在B的构造函数主体执行前会执行赋值操作,declareAndSet=”value in declared”;接着执行B构造器的主体。

-EOF-

参考:
探讨Java类中成员变量的初始化方式
Use of Initializers vs Constructors in Java
Double Brace Initialization