Java中变量的初始化顺序

在写一个通用的报警模块时,遇到一个有意思的问题,在调用静态方法时,发现静态方法内部对静态变量引用时,居然抛出了npe,仿佛是因为这个静态变量的初始化在静态方法被调用时,还没有触发,从而导致这个问题,因此今天专门来学习下静态成员的初始化顺序,以及上面这个问题导致的原因

I. 初始化顺序

类的初始化顺序

静态变量, 静态代码快 -》 实例变量(属性,实例代码块,构造方法)

继承关系初始化顺序

父类静态成员,静态代码块 -》 子类静态成员,静态代码块 -》 父类实例变量(属性,实例代码块,构造方法)-》子类实例变量(属性,实例代码块,构造方法)

II. 静态变量初始化顺序

类初始化时,会优先初始化静态成员,那么一个类中有多个静态成员时,如何处理的?

下面是一个使用静态成员,静态代码块,静态方法的测试类,那么下面的输出应该是怎样的呢?

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
public class StaticTest {
static class A {
public A(int i) {
System.out.println("a init! " + i);
}
}
static class B {
public B(int i) {
System.out.println("b init! " + i);
}
}

private static A a1 = new A(1);
private static B b1;

private static int num;

private static B b2 = new B(2);
private static A a2 = genA(2);

static {
b1 = new B(1);
}

private static A genA(int i) {
System.out.println("gen A: " + i);
return new A(i);
}

private static B genB(int i) {
System.out.println("gen B: " + i);
return new B(i);
}

public static void doSome() {
System.out.println("static function doSome called! a3!=null : " + (a3 != null) + " | num > 0 : " + num);
}

private static A a3;
private static B b3;

static {
System.out.println("num : " + num);
num = 10;
a3 = genA(3);
b3 = genB(3);
}

public static void main(String[] args) {
doSome();
}
}

输出如下

1
2
3
4
5
6
7
8
9
10
11
a init! 1
b init! 2
gen A: 2
a init! 2
b init! 1
num : 0
gen A: 3
a init! 3
gen B: 3
b init! 3
static function doSome called! a3!=null : true | num > 0 : 10

从实际的输出结果来看:

  • 初始化的顺序比较清晰了,压根就是根据初始化代码的先后顺序来的,
  • 且在调用静态方法时,静态方法内部的静态成员已经被初始化

那么问题来了,如果在某个静态成员初始化的时候抛出了异常,会怎样?

那么稍稍改一下上面的代码,加一个主动抛异常的case

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
public class StaticTest {

static class A {
public A(int i) {
System.out.println("a init! " + i);
}
}

static class B {
public B(int i) {
System.out.println("b init! " + i);
}
}


private static A a1 = new A(1);
private static B b1;

private static int num;

private static B b2 = new B(2);
private static A a2 = genA(2);

static {
b1 = new B(1);
}

private static A genA(int i) {
System.out.println("gen A: " + i);
return new A(i);
}

private static B genB(int i) {
System.out.println("gen B: " + i);
return new B(i);
}

private static A aError = genError();
private static A genError() {
System.out.println("gen error!");
throw new RuntimeException();
// return new A(10);
}

public static void doSome() {
System.out.println("static function doSome called! a3!=null : " + (a3 != null) + " | num > 0 : " + num);
}

private static A a3;
private static B b3;

static {
System.out.println("num : " + num);
num = 10;
a3 = genA(3);
b3 = genB(3);
}




public static void main(String[] args) {
doSome();
}
}

此时输出:

1
2
3
4
5
6
7
8
a init! 1
b init! 2
gen A: 2
a init! 2
b init! 1
gen error!
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.RuntimeException

也就是说,初始化异常之后的代码将不会在继续执行

那么第二个问题来了,前面说到哪个问题是什么情况

最开始说到,在调用类的静态方法时,发现本该被初始化的静态成员,依然是null,从上面的分析来说,唯一的可能就是在成员变量初始化的过程中,出现了异常

那么,就有另一个问题了,初始化就报错了,这个类的静态方法还能被调用执行么(加入这个静态方法不依赖内部的静态成员)?

将前面的 genA()方法的private去掉,改成默认的访问范围,然后下面给出一个演示:

post.gif

通过这个演示,也挺有意思的,第一次访问,会抛出一个初始化异常;但是再调用一次,结果发现居然正常执行了;但是调用public方法时,每次都是抛异常

导致这个问题的原因,还有待考究,但是前面这个问题的答案,估摸着和下面差不多了(但是不敢确定,有待大神指点)

  • 理论上类初始化失败,应该就不允许被调用了
  • 但是某些情况下,可以绕过这个限制

III. 成员变量的初始化

测试case也比较简单,把前面的代码中的static去掉即可, 输出

1
2
3
4
5
6
7
8
9
10
11
a init! 1
b init! 2
gen A: 2
a init! 2
b init! 1
num : 0
gen A: 3
a init! 3
gen B: 3
b init! 3
static function doSome called! a3!=null : true | num > 0 : 10

依然是根据初始化代码的先后顺序进行的

当然如果出现异常的情况,和前面的结果类似,不再赘述

IV. 小结

1. 初始化顺序

类的初始化顺序

静态变量, 静态代码快 -》 实例变量(属性,实例代码块,构造方法)

继承关系初始化顺序

父类静态成员,静态代码块 -》 子类静态成员,静态代码块 -》 父类实例变量(属性,实例代码块,构造方法)-》子类实例变量(属性,实例代码块,构造方法)

相同等级的初始化的先后顺序,是直接依赖代码中初始化的先后顺序

2. 初始化异常时

理论上,类初始化中抛出了异常,那么这个类将无法被classLoader正确的加载,因此也无法有效的使用这个类

但是不排除某些情况下,依然强行的使用了这个类(如上面gif图中的演示),这个原理还不太清晰,也有可能是idea的debug功能有什么黑科技?

注意

因此,请格外注意,在初始化代码中,请确保不会有抛出异常,如果无法把控,不妨新建一个init()方法来实现初始化各种状态,然后在代码中主动调用好了

V. 其他

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见解不全,如有问题,欢迎批评指正

扫描关注,java分享

QrCode