[java复习] 初始化

概要

这一章的复习主要涵盖 类的初始化触发条件,类初始化的顺序,构造器的性质特点,成员初始化,以及 数组初始化。这里整理的都是个人觉得比较重要重新认真分析的东西,不一定全部为基础,如果有不准确的地方,还望大佬从评论区指出,感激不尽。

文章中的所有的自定义的测试都在自己的 java-review 仓库中,本文对应的链接:

类的初始化

类初始化的触发

当出现调用的时候,就会触发类加载

  1. 4 条字节码指令
字节码指令 对应的情况
new 使用 new 关键字实例化对象
getstatic 读取一个类的静态字段 (被 final 修饰、已在编译期把结果放入常量池的静态字段除外)
putstatic 设置一个类的静态字段 (同上)
invokestatic 调用一个类的静态方法
  1. java.lang.reflect 包的方法对类进行 反射调用 的 还没有进行初始化的类

  2. 初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要 先触发其父类的初始化

  3. 用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类

  4. JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化

被动引用

被动引用不会触发类的初始化

  1. 调用子类的中继承自父类的静态属性,只触发父类的类加载,不会触发子类的类加载,只触发拥有该属性的类型的初始化

  2. 测试通过创建指定类型的数组,不会触发类的初始化

  3. 测试调用类的/类继承的 static final 属性

    • static final 修饰的为基本数据类型的时候,在 准备阶段 中就将这个常量进行了赋值并放倒了 类的常量池 中,不会触发静态成员初始化

    • static final 成员变量使用调用 static 方法进行赋值的时候,不会在准备阶段完成初始化,只有在类加载的静态变量/域初始化的时候后才会完成初始化

目录结构

1
2
3
4
passivereference
SubClass.java
SuperClass.java
Test.java
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
// test

class SubClass extends SuperClass {
static final int subStaticFinalValue = 120;
static final int subStaticFinalValue2 = initFinalVal();
static {
System.out.println("subclass static block");
}

private static int initFinalVal() {
System.out.println("init final value2");
return 10;
}

}


class SubClass extends SuperClass {
static final int subStaticFinalValue = 120;

static {
System.out.println("subclass static block");
}

}

public class Test {

private static void subClassUseSuperStaticParam() {
System.out.println(SubClass.value);

/* Output:

super static block
123
*///:~
}

private static void array() {
SubClass[] subClassArray = new SubClass[2];

/* Output:

*///:~
}

private static void staticFinalParam() {
System.out.println(SubClass.subStaticFinalValue);
System.out.println(SubClass.supStaticFinalValue);
// System.out.println(SubClass.subStaticFinalValue2);

/* Output:

1. 不开启 SubClass.supStaticFinalValue

200
200
*///:~

/* Output:

2. 开启 SubClass.supStaticFinalValue

200
200
super static block
init final value2
subclass static block
10
*///:~
}

}

初始化顺序

  1. 第一次 调用一个类的 构造器 或者 静态属性/方法 的时候就类加载器会加载这个类,完成 静态属性/域的初始化
    1. 父类 -> 子类
    2. 按照静态属性/域 声明顺序
  1. new 关键为对象分配空间,此时所有的成员变量都被设置成了默认值(默认的零值)
  1. 非静态成员属性初始化
    • 父类 -> 子类
  1. 调用构造器
    • 父类 -> 子类

初始化顺序的验证

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
// java test

public class InitOrder {

private static int k = 0;
private static InitOrder t1 = new InitOrder("obj 1-> constructor");
private static InitOrder t2 = new InitOrder("obj 2-> constructor");
private static int i = print("static-param");
private static int n = 99;
private int a = 0;
public int nonStatic = print("non-static param");

{
print("non-static block");
}

static {
print("static block");
}

private InitOrder(String str) {
System.out.format("%2d: i=%-2d, n=%-3d, %s\n", (++k), i, n, str);
++i; ++n;
}

private static int print(String str) {
System.out.format("%2d: i=%-2d, n=%-3d, %s\n", (++k), i, n, str);
++n;
return ++i;
}

public static void main(String args[]) {
InitOrder t = new InitOrder("initOrder-> constructor");


/* Output:

1: i=0 , n=0 , non-static param
2: i=1 , n=1 , non-static block
3: i=2 , n=2 , obj 1-> constructor

4: i=3 , n=3 , non-static param
5: i=4 , n=4 , non-static block
6: i=5 , n=5 , obj 2-> constructor

7: i=6 , n=6 , static-param
8: i=7 , n=99 , static block
9: i=8 , n=100, non-static param
10: i=9 , n=101, non-static block
11: i=10, n=102, initOrder-> constructor
*///:~
}


}
  1. InitOrder 拥有 main 方法,触发了类加载器

  2. 初始化了 t 的静态成员变量 k

  3. 调用了 t1 = new InitOrder("obj 1-> constructor"); ,初始化 t1 静态成员变量

    1. 初始化 t1 的 非静态成员变量/域
    2. 调用构造器,完成了 t1 的初始化
  4. 调用 t2 = new InitOrder("obj 2-> constructor");,初始化 t2 静态成员变量

    1. 初始化了 t2 的非静态成员变量
    2. 调用构造器,完成了 t2 的初始化

    (这两个静态对象引用初始化的时候,都忽略了剩余的静态属性,直接初始化非静态成员变量)

  5. 初始化 t 的静态块

  6. 初始化 t 的非静态成员变量/块

  7. 调用 t 的构造器,完成初始化

静态属性/域

一个类的所有的对象都公用 static 静态属性

构造器

构造器的性质

  1. 使用 new 关键字的时候就会为对象分配空间,并调用指定的构造器,构造器只用于完成初始化
  2. 构造器不是 static 方法,具体证明在后面的特使代码中
字节码指令 用途
invokestatic 用于调用类(静态)方法
invokespecial 用于调用实例方法,特化于 super 方法调用、private 方法调用与构造器调用
invokevirtual 用于调用一般实例方法(包括声明为 final 但不为 private 的实例方法)
invokeinterface 用于调用接口方法
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
// test

public class Init {

private int value;

private Init() {}

private void method() {}

private static void staticMethod() {}

public static void main(String[] args) {

Init init = new Init(); // 调用 new Init() => invokespecial

init.method(); // 调用非静态方法 => invokespecial

init.staticMethod(); // 通过对象引用来调用 static 方法 (1) => invokestatic

staticMethod(); // 直接调用 static 方法 (2) => invokestatic
}

}


Compiled from "Init.java"
public class com.example.review.initialization.Init {
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/example/review/initialization/Init
3: dup

/* 1. 调用 Init() 构造器 */
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1

/* 2. 调用对象的非静态方法 */
9: invokespecial #4 // Method method:()V
12: aload_1
13: pop

/* 3. 通过对象引用调用静态方法 */
14: invokestatic #5 // Method staticMethod:()V

/* 4. 直接调用静态方法 */
17: invokestatic #5 // Method staticMethod:()V
20: return
}

从反编译的代码中可以看出,调用构造器和实例方法所用的字节码指令相同,所以可以在调用构造器的时候,其实已经完成了对象实例的空间分配,即已经存在了这个对象了。

调用构造器方法的作用 为了将对象实例进行初始化

默认(无参) 构造器

  • 类中没有声明任何构造器的时候,编译器会自动添加一个构造器,即 public ClassName() {} 不带有任何参数的 默认构造器,也叫 无参构造器
  • 类中声明了构造器时,编译器就不会添加默认构造器,此时再调用默认构造器的是时候就会 报错

方法重载

构造器本身就是一种特殊的方法,因为同样支持重载,详细的方法重载会放到后面的基本对象方法中 这里先留一个坑

java 中支持方法的重载,区分重载的依据

  1. 参数类型
  2. 参数个数
  3. 参数顺序

成员的初始化

类的成员变量
在对象空间分配的过程中,给类的所有的成员变量赋以默认的零值(这里同样请参看数据类型),然后才开始进行类的初始化过程

局部变量

编译器不会对局部变量赋予默认值,因此在使用前 必须进行初始化,否则就会编译出错

指定初始化

初始化的顺序为自上而下的声明顺序,在开始非静态成员的初始化的时候,类对象其实已经分配完成,此时就可以调用的类的非静态方法

###构造器初始化

一般也经常自写一个 init() 初始化方法在构造器中进行调用,用来初始化指定的成员变量

静态数据的初始化

static 关键字只能用于域,不能用于局部变量

数组的初始化

数组类本身不是通过类加载器创建的,它是由 java 虚拟机 直接创建的

编译器不允许指定数组的大小 int[12] arr;,这就刚好印证了 java 是一种面向对象的语言,arr 实际上是指向一个数组对象的 引用

基本数据类型和数组的关系

  • 类似于 int[] 的基本数据类型数组同样是一个对象,可以使用 new 关键字来声明
  • 单个的基本数据类型不能这样声明

数组类创建规则

name 类型名 含义
Element Type 元素类型 去掉所有维度后的类型
Element Type 组件类型 去掉一个维度的类型

元素类型最终需要 类加载器 创建

一个数组类(下面简称为 c )创建过程就遵循以下规则

  1. 组件类型为 引用类型,就递归地去加载这个组件类型,数组 C 将在加载该组件类型的类加载器的类名称空间上被标识

  2. 组件类型为 基本数据类型,Java 虚拟机将会把数组 C 标记为与引导类加载器关联。

  3. 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public

可变参数类型

参编参数列表可以接受 一个数组类型 ,或者是 多个指定类型

java 的自动装箱机制对数组类型是不起作用的,使用可变参数列表,可以将传入的基本类型都进行自动装箱/拆箱操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// test

public class Varargs {

private static void cout(Integer ... vars) {}

public static void main(String[] args) {
Integer[] vars1 = new Integer[]{1, 2, 3};
int[] vars2 = {4, 3, 2, 1, }; // 可以以 , 结尾

cout(vars1);
//! cout(vars2); // 基本类型的数组不能进行自动装箱和拆箱
cout(4, 3, 2, 1); // 完成了自动装箱/拆箱,不能以 , 结尾
}

}

枚举类型

  1. 枚举类不能继承
  2. 每个枚举具名值都是一个该枚举类的实例对象
  3. 支持 switch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// test

public enum _Enum {
ENUM1, ENUM2, ENUM3
}


public class Test {

public static void main(String[] args) {
for (_Enum e : _Enum.values())
System.out.println("Enum.name: " + e + ", order: " + e.ordinal());
switch (_Enum.ENUM1) {

}
}

}