[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_getStaticREF_putStaticREF_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) {

}
}

}