前面讲过,java虚拟机只有在程序首次主动使用一个类或接口的时候才会初始化它。只有6种活动被看作是程序对类和接口的主动使用:
1、创建类的实例。例如new语句创建实例,或者通过反射、克隆及序列号手段来创建实例。
2、调用类的静态方法。
3、访问某个类或接口的静态变量或者对该静态变量赋值。
4、调用java API中某些反射方法,比如调用Class.forName("Worker")方法,加入Worker类还没有被初始化,那么forName()方法就会初始化Worker类,然后返回代表这个Worker类的Class实例。forName()方法是java.lang.Class类的静态方法。
5、初始化一个类的子类。例如对Sub类的初始化,可看作是它对父类Base类的主动使用,因此会先初始化Base类。
6、java虚拟机启动时被注明为启动类的类。例如对于“java Sample”命令,Sample类就是启动类,java虚拟机会先初始化它。
除上述6种情形外,其它使用java类的方式都被看作是被动使用,都不会导致类的初始化。下面结合具体的例子来解释类的初始化时机。
1、对于final类型的静态变量,如果在编译时期就能计算出变量的取值,那么这种变量称为编译时常量。java程序中对类的编译时常量的使用,被看作是对类的被动使用,不会导致类的初始化。例如以下Tester类的静态变量a就是编译时常量,java编译器在编译的时候就能计算出a的取值为6。当Sample访问Tester.a的时候,并没有导致Tester类的初始化。
public class Tester {
public static final int a = 2*3;//变量a是编译时常量
//public stataic final int a = (int)(Math.random()*5); //变量a不是编译时常量
static {
System.out.println("init Tester");
}
}
public class Sample {
public static void main(String[] args) {
System.out.println("a="+Tester.a);
}
}
以上程序的打印结果为:a=6。
当java编译器生成Sample类的.class文件的时候,它不会在main()方法的字节码流中保存一个表示Tester.a的符号引用,而是直接在字节码流中嵌入常量值6。因此当程序访问Tester.a的时候,客观上无需初始化Tester类。
注:当java虚拟机加载并连接Tester类时,不会在方法区内为它的编译时常量a分配内存。
2、对于final类型的静态变量,如果在编译时不能计算出变量的取值,那么程序对类的这种变量的使用,被看作是对类的主动使用,会导致类的初始化。例如把以上Tester类做如下修改:
public class Tester {
public static final int a = (int)(Math.random()*10)/10+1;//变量a不是编译时常量
static {
System.out.println("init Tester");
}
}
由于编译器不会计算出变量a的取值,因此变量a不是编译时常量。当Sample访问Tester.a的时候,java虚拟机会先初始化Tester类,湿的变量a在方法区on更有特定的内存和初始值。此时Sample类的main()方法的打印结果为:
init Tester
a=1
3、当java虚拟机初始化一个类时,要求它的所有的父类都已经被初始化,但是这条规则并不适用于接口。在初始化一个类时,并不会先初始化它所实现的接口。在初试一个接口的时候,并不会先初始化它的父接口。
4、只有当程序访问的静态变量或者静态方法的却在当前类或接口中定义时,才可看作是对类或接口的主动使用。例如以下程序:
class Base {
static int a = 1;
static {
System.out.println("init Base");
}
static void method(){
System.out.println("method of Base");
}
}
class Sub extends Base {
static {
System.out.println("init Sub");
}
}
public class Sample {
public static void main(String[] args) {
System.out.println("a="+Sub.a);
Sub.method();
}
}
以上程序打印结果为:
init Base
a=1
method of Base
因为Sub.a的属性a是定义在父类中,所以会初始化父类,并不会初始化Sub类。等到Sub.method()方法的时候,才回去初始化Sub类。
5、调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化,例如以下程序,先用系统类加载器加载ClassA,尽管ClassA被加载,但是并没有初始化。当程序调用Class类的静态方法forName("ClassA")方法显示初始化ClassA时,才是对ClassA的主动使用,将导致ClassA被初始化,它的静态代码块被执行。
class ClassA{
static {
System.out.println("now init ClassA");
}
}
public class ClassB {
public static void main(String[] args)throws Exception{
ClassLoader loader = ClassLoader.getSystemClassLoader();//获得系统类加载器
Class objClass = loader.loadClass("ClassA");//加载ClassA
System.out.println("after load ClassA");
System.out.println("before init ClassA");
objClass = Class.forName("ClassA");//初始化ClassA
}
}
打印结果如下:
after load ClassA
before init ClassA
now init ClassA