除了使用反射以外【Class.forName("类的全限定名")】,Java还提供了另一种方法来生成对Class对象的引用,即使用 类字面常量:
类.class;
使用这种方式确实比使用反射更简单,而且更安全,因为它在编译期就会受到检查【因此不需要至于try-catch块中】。
在类被使用前实际做了以下三个步骤:
第一:加载。这是由类加载器执行的。该步骤将查找这个字节码文件(通常在classpath所指定的路径中查找,但这并非是必需的),并创建该字节码文件对象。
第二:链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用。
第三:初始化。执行静态初始化器和静态初始化块,如果该类具有超类,则对其初始化。
但是如果使用.class的方式创建对Class对象的引用时,不会自动地初始化该Class对象【也就是上述的步骤三】。使用这种方式,它的初始化步骤被延迟到了对静态方法(构造器隐式地是静态的)或者非常数(不被final修饰)静态域进行首次引用时才执行。
使用类字面常量:
package 类型信息; public class InitA { //非常数 public static int initInt = 10; //常数 public static final int initInt2 = 20; //静态域 static { System.out.println("初始化了InitA"); } //静态方法 public static void method() { System.out.println("静态方法"); } }
测试:
package 类型信息; public class Test { public static void main(String[] args) { Class<InitA> inita = InitA.class; } }
运行测试程序控制台没有任何输出。这说明没有执行初始化。
但是当我们首次调用InitA类里的非常数静态域时:
package 类型信息; public class Test { public static void main(String[] args) { //1.加载 2.链接 Class<InitA> inita = InitA.class; //3.初始化【被延迟】 System.out.println(InitA.initInt); } }
控制台:
初始化了InitA
10
首次调用静态方法时:
package 类型信息; public class Test { public static void main(String[] args) { //1.加载 2.链接 Class<InitA> inita = InitA.class; //3.初始化【被延迟到非常数静态域】 //System.out.println(InitA.initInt); //3.初始化【被延迟到非静态方法】 InitA.method(); } }
控制台:
初始化了InitA
静态方法
测试调用常数静态域会不会初始化?
package 类型信息; public class Test { public static void main(String[] args) { //1.加载 2.链接 Class<InitA> inita = InitA.class; //3.初始化【被延迟到非常数静态域】 //System.out.println(InitA.initInt); //3.初始化【被延迟到非静态方法】 //InitA.method(); //调用常数静态域会初始化吗? System.out.println(InitA.initInt2); } }
控制台:
20
可见:常数静态域的调用并不会使得其被初始化。【也说明:类不被初始化 常数静态域【编译时常量】 就可以被读取到,而非常数静态域就必须初始化后才能被读取,对非常数静态域总是要求其被读取之前,要先进行链接和初始化(初始化该存储空间)】
以上说明了使用.class创建的对象引用时,初始化过程被延迟到非常量静态域或静态方法了。
使用反射呢?
package 类型信息; public class Test { public static void main(String[] args) { //使用反射 try { Class.forName("类型信息.InitA"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
控制台:
初始化了InitA
可见:使用反射时,类被使用之前所需的三个步骤都被执行了。