Java——深入理解Class对象(二):Class对象的加载及其获取方式

上一篇博客Java——深入理解Class对象(一)带大家简单认识了一下Java中Class对象。

现在带大家了解一下Class对象的加载及其获取方式。

1.Class对象的加载

Java——深入理解Class对象(一)我们已提到过,Class对象是由JVM加载的,那它必然不会是胡乱加载的,肯定有加载时机。

实际上,所有的类都是在对其第一次使用时动态加载到JVM中的。当程序创建第一个对类的静态成员引用时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件)。这里需要注意,使用new操作符创建类的新实例对象也会被当作对类的静态成员的引用(构造函数也是类的静态方法)。

由此我们就可以知道,Java程序在它们开始运行之前并非被完全加载到内存的,其各个部分是按需加载。

因此,在使用该类时,类加载器首先会检查这个类的Class对象是否已被加载(类的实例对象创建时依据Class对象中类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找.class文件(编译后Class对象被保存在同名的.class文件中),在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良Java代码(这是java的安全机制检测),完全没有问题后就会被动态加载到内存中,此时相当于Class对象也就被载入内存了(毕竟.class字节码文件保存的就是Class对象),同时也就可以被用来创建这个类的所有实例对象。

下面通过一个简单例子来说明Class对象被加载的时机问题(例子引用自Thinking in Java):

package javatest;

public class SweetShop {
	  public static void print(Object obj) {
	    System.out.println(obj);
	  }
	  public static void main(String[] args) {  
	    print("inside main");
	    new Candy();
	    print("After creating Candy");
	    try {
	      Class.forName("javatest.Gum");
	    } catch(ClassNotFoundException e) {
	      print("Couldn't find Gum");
	    }
	    print("After Class.forName(\"javatest.Gum\")");
	    new Cookie();
	    print("After creating Cookie");
	  }
}


class Candy {
  static {   System.out.println("Loading Candy"); }
}
class Gum {
  static {   System.out.println("Loading Gum"); }
}
class Cookie {
  static {   System.out.println("Loading Cookie"); }
}

在上述代码中,每个类Candy、Gum、Cookie都存在一个static语句,这个语句会在类第一次被加载时执行,这个语句的作用就是告诉我们该类在什么时候被加载。

我们来看一下执行结果:

从结果来看,new一个Candy对象和Cookie对象,构造函数将被调用,属于静态方法的引用,Candy类的Class对象和Cookie的Class对象肯定会被加载,毕竟Candy实例对象的创建依据其Class对象。

比较有意思的是Class.forName(“javatest.Gum”)语句也使得Gum的Class对象被加载。​​​​​​如下图​

forName方法是Class类的一个static成员方法,所有的Class对象都源于这个Class类,因此Class类中定义的方法将适应所有Class对象。这里通过forName方法,我们可以获取到Gum类对应的Class对象引用。因此,如果Gum类之前没有被加载过的话,调用forName方法将会导致Gum类被加载。下面就来认识一下forName方法
 

2.Class.forName方法

通过上面的案例,我们可以知道Class.forName()方法的调用将会返回一个对应类的Class对象,因此如果我们想获取一个类的运行时类型信息并加以使用时,可以调用Class.forName()方法获取Class对象的引用,这样做的好处无需通过持有该类的实例对象引用而去获取Class对象。

什么叫通过该类的实例对象引用而去获取Class对象呢?

下面举一个例子对比这两种方法

将上文程序中的main函数修改为:

	  public static void main(String[] args) {

		    try{
		      //通过Class.forName获取Gum类的Class对象
		      Class clazz=Class.forName("javatest.Gum");
		      System.out.println("forName=clazz:"+clazz.getName());
		    }catch (ClassNotFoundException e){
		      e.printStackTrace();
		    }

		    //通过实例对象获取Gum的Class对象
		    Gum gum = new Gum();
		    Class clazz2=gum.getClass();
		    System.out.println("new=clazz2:"+clazz2.getName());

		  }

上边第二种方法就是通过一个实例对象获取一个类的Class对象,其中的getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用。

这时候运行结果如下:

注意:调用forName方法时需要捕获一个名称为ClassNotFoundException的异常,因为forName方法在编译器是无法检测到其传递的字符串对应的类是否存在的,只能在程序运行时进行检查,如果不存在就会抛出ClassNotFoundException异常。

比如我将

  Class clazz=Class.forName("javatest.Gum");

改为

  Class clazz=Class.forName("javatest.Aum");

那么程序运行的时候就会报错,如下图

3.Class字面常量

在Java中存在另一种方式来生成Class对象的引用,它就是Class字面常量,如下:

//字面常量的方式获取Class对象

Class clazz = Gum.class;

这种方式相对前面两种方法更加简单,更安全。

因为它在编译器就会受到编译器的检查,同时由于无需调用forName方法效率也会更高,因为通过字面量的方法获取Class对象的引用不会自动初始化该类。

更加有趣的是,字面常量的获取Class对象引用方式不仅可以应用于普通的类,也可以应用用接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助。由于基本数据类型还有对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下。(一般情况下更倾向使用.class的形式,这样可以保持与普通类的形式统一。

boolean.class = Boolean.TYPE;
char.class = Character.TYPE;
byte.class = Byte.TYPE;
short.class = Short.TYPE;
int.class = Integer.TYPE;
long.class = Long.TYPE;
float.class = Float.TYPE;
double.class = Double.TYPE;
void.class = Void.TYPE;

前面提到过,使用字面常量的方式获取Class对象的引用不会触发类的初始化,这里我们可能需要简单了解一下类加载的过程,如下:

解释如下:

  • 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
  • 链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。
  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。
     

由此可知,我们获取字面常量的Class引用时,触发的应该是加载阶段,因为在这个阶段Class对象已创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化。下面通过小例子来验证这个过程:

代码如下:

package javatest;
import java.util.*;

class Initable {
  //编译期静态常量
  static final int staticFinal = 47;
  //非编期静态常量
  static final int staticFinal2 =
    ClassInitialization.rand.nextInt(1000);
  static {
    System.out.println("Initializing Initable");
  }
}

class Initable2 {
  //静态成员变量
  static int staticNonFinal = 147;
  static {
    System.out.println("Initializing Initable2");
  }
}

class Initable3 {
  //静态成员变量
  static int staticNonFinal = 74;
  static {
    System.out.println("Initializing Initable3");
  }
}

public class ClassInitialization {
  public static Random rand = new Random(47);
  public static void main(String[] args) throws Exception {
    //字面常量获取方式获取Class对象
    Class initable = Initable.class;
    System.out.println("After creating Initable ref");
    //不触发类初始化
    System.out.println(Initable.staticFinal);
    //会触发类初始化
    System.out.println(Initable.staticFinal2);
    //会触发类初始化
    System.out.println(Initable2.staticNonFinal);
    //forName方法获取Class对象
    Class initable3 = Class.forName("javatest.Initable3");
    System.out.println("After creating Initable3 ref");
    System.out.println(Initable3.staticNonFinal);
  }
}

运行结果如下:

从输出结果来看,可以发现,通过字面常量获取方式获取Initable类的Class对象并没有触发Initable类的初始化,这点也验证了前面的分析。

同时我们也可以发现,调用Initable.staticFinal变量时也没有触发初始化,这是因为staticFinal属于编译期静态常量,在编译阶段通过常量传播优化的方式将Initable类的常量staticFinal存储到了一个称为NotInitialization类的常量池中,在以后对Initable类常量staticFinal的引用实际都转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在NotInitialization类的常量池获取,这也就是引用编译期静态常量不会触发Initable类初始化的重要原因。

但在之后调用了Initable.staticFinal2变量后就触发了Initable类的初始化。因为虽然staticFinal2被static和final修饰,但其值在编译期并不能确定因此staticFinal2并不是编译期常量,使用该变量必须先初始化Initable类。Initable2和Initable3类中都是静态成员变量并非编译期常量,引用都会触发初始化。至于forName方法获取Class对象,肯定会触发初始化,这点在前面已分析过。

到这里,几种获取Class对象的方式也都分析完,我们可以得出小结论

  • 获取Class对象引用的方式又3种,即通过继承自Object类的getClass方法Class类的静态方法forName以及字面常量的方式”.class”。
  • 实例类的getClass方法和Class类的静态方法forName都将会触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化。
  • 初始化是类加载的最后一个阶段,也就是说完成这个阶段后类也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),在这个阶段,才真正开始执行类中定义的Java程序代码或者字节码。
     

关于类加载的初始化场景,大家可以参照Java——类什么时候会进行初始化

Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

参考文章:https://blog.csdn.net/javazejian/article/details/70768369

猜你喜欢

转载自blog.csdn.net/Searchin_R/article/details/84592516
今日推荐