理解类在JVM中什么时候被加载和初始化是Java编程语言中的基础概念,正因为有了Java语言规范,我们才可以清晰的记录和解释这个问题,但是很多Java程序员仍然不知道什么时候类被加载,什么时候类被初始化,类加载和初始化好像让人很困惑,对初学者难以理解,在这篇教程中我们将看看类加载什么时候发生,类和接口是如何被初始化的,我并不会拘泥于类加载器的细节或者说类加载器的工作方式。仅仅使这篇文章更加专注和简结。
类什么时候加载
类的加载是通过类加载器(Classloader)完成的,它既可以是饿汉式[eagerly load](只要有其它类引用了它就加载)加载类,也可以是懒加载[lazy load](等到类初始化发生的时候才加载)。不过我相信这跟不同的JVM实现有关,然而他又是受JLS保证的(当有静态初始化需求的时候才被加载)。
类什么时候初始化
加载完类后,类的初始化就会发生,意味着它会初始化所有类静态成员,以下情况一个类被初始化:
- 实例通过使用new()关键字创建或者使用class.forName()反射,但它有可能导致ClassNotFoundException。
- 类的静态方法被调用
- 类的静态域被赋值
- 静态域被访问,而且它不是常量
- 在顶层类中执行assert语句
反射同样可以使类初始化,比如java.lang.reflect包下面的某些方法,JLS严格的说明:一个类不会被任何除以上之外的原因初始化。
类是如何被初始化的
现在我们知道什么时候触发类的初始化了,他精确地写在Java语言规范中。但了解清楚 域(fields,静态的还是非静态的)、块(block静态的还是非静态的)、不同类(子类和超类)和不同的接口(子接口,实现类和超接口)的初始化顺序也很重要类。事实上很多核心Java面试题和SCJP问题都是基于这些概念,下面是类初始化的一些规则:
- 类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
- 超类早于子类和衍生类的初始化
- 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化即使静态域被子类或子接口或者它的实现类所引用。
- 接口初始化不会导致父接口的初始化。
- 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
- 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类
初始化例子
这是一个有关类被初始化的例子,你可以看到哪个类被初始化
|
从上面结果可以看出:
- 超类初始化早于子类
- 静态变量或代码块初始化早于非静态块和域
- 没使用的类根本不会被初始化,因为他没有被使用
再来看一个例子:
|
分析:
- 这里的初始化发生是因为有静态域被访问,而且不一个编译时常量。如果声明的”familyName”是使用final关键字修饰的编译时常量使用(就是上面的注释代码块部分)超类的初始化就不会发生。
- 尽管静态与被子类所引用但是也仅仅是超类被初始化
还有另外一个例子与接口相关的,JLS清晰地解释子接口的初始化不会触发父接口的初始化。强烈推荐阅读JLS14.4理解类加载和初始化细节。以上所有就是有关类被初始化和加载的全部内容。
=======================================
=======================================
标题起得略拗口,大概意思就是说在一个Java类中,域和构造方法的调用顺序。
1. 没有继承的情况
单独一个类的场景下,初始化顺序为依次为 静态数据,继承的基类的构造函数,成员变量,被调用的构造函数。
其中静态数据只会初始化一次。
package com.khlin.binding.test;
public class App2 {
public static void main(String[] args) {
Son son = new Son();
}
}
class Son {
public Son() {
System.out.println("this is son.");
}
public Son(int age) {
System.out.println("son is " + age + " years old.");
}
private Height height = new Height(1.8f);
public static Gender gender = new Gender(true);
}
class Height {
public Height(float height) {
System.out.println("initializing height " + height + " meters.");
}
}
class Gender {
public Gender(boolean isMale) {
if (isMale) {
System.out.println("this is a male.");
} else {
System.out.println("this is a female.");
}
}
}
输出:
2. 继承的情况
稍微修改一下代码,添加两个基类,让Son继承Father, Father继承Grandpa。
继承的情况就比较复杂了。由于继承了基类,还将往上回溯,递归地调用基类的无参构造方法。
在我们的例子中,在初始化静态数据后,会先往上追溯,调用Father的默认构造方法,此时再往上追溯到Grandpa的默认构造方法。
注:如果在子类的构造方法中,显式地调用了父类的带参构造方法,那么JVM将调用指定的构造方法而非默认构造方法。
基类和子类均有静态数据,成员变量和构造方法的场景
我们继续修改代码,让其最终呈现如下:
1 package com.khlin.binding.test;
2
3 public class App2 {
4 public static void main(String[] args) {
5 Son son = new Son();
6 }
7 }
8
9 class Grandpa {
10 public Grandpa() {
11 System.out.println("this is grandpa.");
12 }
13
14 public Grandpa(int age) {
15 System.out.println("grandpa is " + age + " years old.");
16 }
17
18 private Height height = new Height(1.5f);
19
20 public static Gender gender = new Gender(true, "grandpa");
21 }
22
23 class Father extends Grandpa {
24
25 public Father() {
26 System.out.println("this is father.");
27 }
28
29 public Father(int age) {
30 System.out.println("father is " + age + " years old.");
31 }
32
33 private Height height = new Height(1.6f);
34
35 public static Gender gender = new Gender(true, "father");
36 }
37
38 class Son extends Father {
39
40 public Son() {
41 super(50);
42 System.out.println("this is son.");
43 }
44
45 public Son(int age) {
46 System.out.println("son is " + age + " years old.");
47 }
48
49 private Height height = new Height(1.8f);
50
51 public static Gender gender = new Gender(true, "son");
52 }
53
54 class Height {
55 public Height(float height) {
56 System.out.println("initializing height " + height + " meters.");
57 }
58 }
59
60 class Gender {
61 public Gender(boolean isMale) {
62 if (isMale) {
63 System.out.println("this is a male.");
64 } else {
65 System.out.println("this is a female.");
66 }
67 }
68
69 public Gender(boolean isMale, String identify) {
70 if (isMale) {
71 System.out.println(identify + " is a male.");
72 } else {
73 System.out.println(identify + " is a female.");
74 }
75 }
76 }
最后输出会是什么呢?
参考下面另一个案例的分析。链接:
在我们的示例中,加载顺序应该是这样的:
Grandpa 静态数据
Father 静态数据
Son 静态数据
Grandpa 成员变量
Grandpa 构造方法
Father 成员变量
Father 构造方法
Son 成员变量
Son 构造方法
所以输出如下: