在6.1小节中曾经讲过:创建对象前会完成类加载的操作。实际上,如果在程序中使用new关键字来创建一个对象,虚拟机会在创建对象之前需要完成一系列准备工作,类的加载只是这些工作中的一步。具体来说,这一系列工作可以分为类的加载、连接和初始化三步。多数情况下虚拟机都是连续完成这些工作的,因此这三个步骤也可以统称为“类的加载”或“类的初始化”,本小节将详细讲解这些步骤的过程和原理。
19.1.1类的加载
在Java语言中,每一个类都会编译成一个独立的字节码文件(.class文件),因此字节码文件中记录着类的各种信息,包括类有哪些属性,属性的类型和名称是什么,访问度是什么等等。类的加载就是把字节码文件读入内存的操作,把字节码读入内存是为了获得记录在字节码文件中关于类的所有信息。类的加载会在很多情况下进行,最典型的情况就是程序中第一次创建某个类的对象时会进行类的加载。当把一个类的信息读取到内存中后,虚拟机会把这个类的信息保存在java.lang包下的Class类的对象中。此处特别需要提醒初学Java的读者:“Class”是一个类的名称,它与表示类的、首字母为小写的“class”关键字是不同的。由此可见:Java虚拟机内存中,每一个Class类对象都保存了一个类(或接口、枚举)的信息。
类的加载是由“类加载器”完成的。类加载器通常都是由Java虚拟机产品提供的,实际上程序员也可以通过继承ClassLoader类来实现自己的类加载器。类加载器能够加载不同来源的字节码文件,例如可以加载工程文件夹中的那些字节码文件,也可以加载.jar文件中那些被打包的字节码文件,甚至可以加载来自于网络的字节码文件。类加载器除了可以完成加载这个操作以外,还可以对一个java源文件进行动态编译并执行加载。
19.1.2类的连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下三个阶段。
- 验证:验证阶段的主要工作检验被加载的类是否有正确的内部结构,并和其他类协调一致
- 准备:类准备阶段负责为类的静态属性分配内存,并设置默认初始值
- 解析:将类的二进制数据中的符号引用替换成直接引用
19.1.3类的初始化
类的连接完成后,就要进行类的初始化。初始化阶段主要负责对类的静态属性进行初始化。连接阶段只是对类的静态属性赋予默认值,而初始化阶段则是为类的静态属性赋予程序员指定的初始值。程序员对类的静态属性指定初始值有两种方式:1、定义类时指定静态属性初始值。2、使用静态块指定静态属性的初始值,例如:
public class Test{
static int a = 5;//①
static int b;
static int c;
static{
b = 6;//②
}
}
在以上代码中,语句①在定义类时指定了静态属性a的初始值,而语句②是在静态块中指定了静态属性的初始值。语句①和②都会被当作类的初始化语句,虚拟机会按从上到下的顺序执行这些初始化语句。初始化语句甚至可以出现在静态属性的定义语句之前,下面的【例19_01】就能够说明类的初始化过程。
【例19_01 类的初始化1】
Exam19_01.java
public class Exam19_01 {
static{
b = 6;//①
}
static int a = 5;
static int b = 9;//②
public static void main(String[] args) {
System.out.println(Exam19_01.b);
}
}
【例19_01】中,语句①是对静态属性b的初始化,这条语句出现在定义静态属性b的语句②之前,但编译器并不会因此报错。语句①把b的初始值设置为6,更靠下的语句②把b的值设置为9,按照从上到下的初始化顺序,语句②会把语句①对b设置的初始值修改为9,因此【例19_01】运行的结果是在控制台上输出9,读者可以自行运行这个程序以观察初始化语句的执行效果。
需要注意:子类的初始化总是晚于父类的初始化的,因此一个类在完成初始化之前虚拟机会先初始化其父类,如果父类还有父类,则更早初始化父类的父类,以此类推。
19.1.4类的初始化时机
前文介绍过:多数情况下类的加载、连接和初始化都是连续完成的,但并不是每次类的加载都会引起类的初始化,下列几种情况会引起类的加载并会在加载和连接之后同时完成初始化。
- 创建类的对象,具体方式包括:使用new关键字来创建对象,通过反射来创建对象,通过反序列化的方式来创建对象。
- 调用某个类的静态方法。
- 访问某个类或接口的类静态属性,或为该静态属性赋值。
- 使用反射方式来强制创建某个类或接口对应的Class的对象。例如:Class.forName("Person");
如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的Class类的对象。关于Class的forName方法请参考18.3节。 - 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
- 直接使用java.exe命令来运行含有main()方法的类,当运行这个类时程序会先初始化该类。
在学习过程中,如果希望检验某个类有没有被初始化,只需要在这个类中添加一个静态块,如果静态块中的代码被执行,说明这个类被初始化了,否则说明这个类没有被初始化。对于一个final修饰的静态属性而言,如果该静态属性的值在编译时就可以确定下来,那么这个静态属性会在编译时直接被替换成一个常量,因此即使程序使用该静态属性也不会导致该类的初始化。反之,如果final 修饰的静态属性的值不能在编译时确定下来,也就是说必须等到运行时才可以确定该静态属性的值,那么程序中访问这个静态属性会导致该类被初始化。下面的【例19_02】就很好的展示了这个特性。
【例19_02 类的初始化2】
Exam19_02.java
class A{
//编译时就能确定值的final静态属性
static final String str = "我喜欢学Java";
static {
System.out.println("A类被初始化");
}
}
class B{
//运行时才能确定值的final静态属性
static final String str = System.currentTimeMillis()+"";
static {
System.out.println("B类被初始化");
}
}
public class Exam19_02 {
public static void main(String[] args) {
System.out.println(A.str);
System.out.println(B.str);
}
}
【例19_02】中,A类和B类各有一个被final关键字修饰的静态属性str。A类的静态属性str在编译时就能确定值,而B类的str值是当前系统时间和一个空字符拼接的结果,因此这个值只有在运行时才能确定。main()方法中输出了A类和B类的str,【例19_02】的运行结果如图19-1所示。
图19-1【例19_02】运行结果
从图19-1可以看出:输出A类的str之前没有执行A类静态块中的代码,这证明调用在编译阶段能够确定值的静态属性不会引起类的初始化。
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。
标签:初始化,静态,19,第十九章,static,加载,属性 From: https://blog.51cto.com/mugexuetang/5984812