惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
C
Check Point Blog
博客园_首页
B
Blog
D
Docker
U
Unit 42
量子位
I
InfoQ
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
美团技术团队
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
Vercel News
Vercel News
Recent Announcements
Recent Announcements
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Google DeepMind News
Google DeepMind News

Xudong's Blog

AI杀死了个人博客 让MDC在各种线程间穿梭自如 新手选购微单的策略 一些让Python代码更快的技巧 HTTP报文结构 Python日志库logging总结 写定向爬虫时遇到的问题 Java中的协变与逆变 给Hexo主题添加LaTeX公式支持 Python3中的Iterator与Iterable Rss订阅源分享 Git Tips Redis的数据库与持久化 Redis中的数据结构 控制反转与依赖注入 Vim命令笔记 Java中的equals和hashCode方法 Java中的基本类型和自动装拆箱 Hello, world. 使用Hexo搭建静态博客
Java中对象域的初始化
Xudong Sun · 2018-10-03 · via Xudong's Blog

正确的初始化很重要

java中,一个类的域(或者说类的属性,类的数据成员)可以是基本数据类型,也可以是对象的引用。对象域如果没有被初始化,会被置为null,如果在置为null的引用上调用方法,我们就会得到一个运行时错误。
在恰当的时间,恰当的位置,正确地初始化对象域是很重要的。

初始化的位置

java中可以在四个位置进行类的对象域初始化:

  • 在域定义时初始化
  • 在类构造器中初始化
  • 在使用类中的对象域之前手动初始化(这被称作懒惰初始化)
  • 使用实例初始化块

要注意上面列出的顺序并不是这些位置初始化的时间顺序。我们经常能看到的初始化方式是1和2。

当然,我们不能忘了一个特殊的存在:静态成员。
如果一个对象域是静态的,那么它的初始化就和类对象的初始化剥离开了。
静态成员的初始化在类被加载时进行,这是我们一定要知道的,但不是这篇文章的重点。

初始化的顺序

下面通过代码来证明这四种初始化发生的时间顺序。
类A拥有一个静态数据成员sstrA,和一个数据成员strA。拥有一个静态初始化块,和一个类初始化块。有一个无参构造函数。override了toString方法,在toString方法中懒惰初始化strA。
而类B结构与A基本相同,并且继承自A。

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
    class A {
static String sstrA = "static string in A";
String strA = "string in A";

static {
System.out.println("A static init block start");
sstrA = "sstrA from init block";
}

{
System.out.println("instance init block start");
strA = "strA from init block";
}

public A() {
System.out.println("constructor start");
}

@Override
public String toString() {
if (strA == null)
strA = "strA from delayed init";
return "static sstrA: " + sstrA + "\nstrA: " + strA;
}
}

class B extends A {
static String sstrB = "static string in B";
String strB = "string in B";

static {
System.out.println("B static init block start");
sstrB = "sstrB from init block";
}

{
System.out.println("instance init block start");
strB = "strB from init block";
}

public B() {
System.out.println("constructor start");
}

@Override
public String toString() {
if (strB == null)
strB = "strB from delayed init";
return super.toString() + "\nstatic sstrB: " + sstrB + "\nstrA: " + strB;
}
}
```java

在main函数中创建这两个类的实例:

```java
A a = new A();
System.out.println("----------------");
System.out.println(a);

System.out.println("\n================\n");

B b = new B();
System.out.println("----------------");
System.out.println(b);

可以得到输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
A static init block start
A instance init block start
A constructor start
----------------
static sstrA: sstrA from init block
strA: strA from init block

================

B static init block start
A instance init block start
A constructor start
B instance init block start
B constructor start
----------------
static sstrA: sstrA from init block
strA: strA from init block
static sstrB: sstrB from init block
strA: strB from init block

根据前半部分的运行结果,我们看到首先在类加载时,静态初始化块被执行了。
然后便是类实例化创建对象的过程,在构造函数被调用前,实例初始化块被首先执行。
根据toString的返回字符串,我们看到显示的内容是实例初始化块中赋值的字符串,说明最一开始在定义处初始化的字符串被实例初始化块覆盖掉。
最终得出一个顺序:

  • 在最一开始,类被加载时,静态相关的初始化会执行
  • 类实例化的第一步,定义处的初始化会被执行
  • 然后是实例初始化块的执行
  • 再后面才是构造器
  • 最后当然是懒惰初始化在必要时得到执行

然后后半部分B类的实例化过程展现了继承存在时初始化的顺序。

因为A类在之前已经被加载过,所以静态初始化块不会被执行,此处只有B类的静态初始化块。

在实例化过程中首先会进行父类A的初始化,然后才是B本身的初始化。

总的来说,java在类成员初始化的顺序上,并没有什么反直觉的地方,只要稍加理解便能掌握。

尾巴

其实,对象域的初始化甚至可以发生在对象的外部,初始化的时间也可以由运行时决定,反射技术将可以达到这一目的,我们可以将其大致归类到惰性初始化中。对象的初始化还可以更深入到JVM层面来剖析,这篇浅显的文章就没有展开来讲。Java中的每个点都有更深层次的细节可以学习,希望我们可以一同探索。