深入理解Class对象
Class类的概念
想要理解RTTI在Java中的工作原理,首先得知道类型信息在运行时是如何表示的。Java用Class类来表示运行时的类型信息,首先必须明确,Class类跟Java API中定义的String、Integer等类以及我们自己定义的类是一样的,是一个实实在在的类,只不过名字特殊点,在JDK的java.lang包中。
那么Class类到底有什么作用呢?是什么的抽象,其实例又表示什么呢?
对于我们自己定义的类,我们用类来抽象现实中的某些事物,比如我们定义名为Dog的类来抽象现实中的狗,然后可以实例化这个类,用这些实例来表示一条黑狗、一条黄狗、我家的狗、你家的狗等等。我们还用Cat类来抽象现实中的猫,用Brid类来抽象现实中的鸟。那么,Dog、Cat、Brid这三个类之间有没有共同特征了,可不可以对这三个类进行抽象呢?
当然可以,我们都知道所有的class都是Object的子类,都有类名,有hashcode,可以判断类型属于class、interface、enum还是annotation。另外可以定义一些方法,比如获取某个方法、获取类型名等等。这样就封装了一个表示类型的类 — Class,用来提取这些类的一些共同特征,表示对这些类(或接口)的抽象。而Dog、Cat、Brid这三个类就分别是Class类的对象。也就是说,每个类都有一个Class对象,即每当我们编写并且编译一个新类,就会产生一个对应的Class对象,被保存在一个同名.class文件(编译后的字节码文件)里。
下面我们来分析一下Class类的源码:
//前一个Class表示这是一个类的声明,第二个Class是类的名称,
<T>表示这是一个泛型类,并实现了四种接口。
public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
//定义了三个静态变量
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
//定义了一个名为registerNatives()的本地方法,并在静态块中调用:
private static native void registerNatives();
static {
registerNatives();
}
// 私有构造函数,只能由JVM调用,创建该类实例
private Class(ClassLoader loader) {
classLoader = loader;
}
/*如果Class对象是一个Java类,返回class full_classname,即class 包名.类名;
比如上面例子的List,返回的就是class java.util.List;
如果是接口,将class改成interface;
如果是void类型,则返回void;
如果是基本类型,返回基本类型。*/
public String toString() {
return (isInterface() ?"interface " : (isPrimitive() ? "" : "class")) + getName();
}
注意:Class类的构造器是private的,这意味着我们无法用new关键字得到一个Class对象。为了生成一个类的Class对象,必须通过运行Java虚拟机(JVM)中的类加载器子系统。
从上我们可以总结出:
- Class类的作用是运行时提供或获得某个对象的类型信息;
- Class类也是类的一种,只是名字和class关键字高度相似;
- Class类的对象表示你创建的类的类型信息,比如你创建一个Dog类,那么,Java编译后就会创建一个包含Dog类型信息的Class对象;
- Class类只有私有构造函数,因此对应的Class对象不能像普通类一样,以 new 操作符的方式创建,只能通过JVM加载。
- 一个class类有且只有一个相对应的Class对象(无论创建多少个实例对象,在JVM中都只有一个Class对象),如下图所示:
Class对象的加载
那么JVM是如何加载这个类的?
当程序创建第一个对类的静态成员的引用时,JVM中的类加载器子系统会将类对应的Class对象加载到JVM中。这个证明构造器也是类的静态方法,尽管构造器前并没有用static关键字修饰。因此,当我们使用new操作符创建一个类的实例对象时,也会被当作对类的静态成员的引用。
可以看出,Java一门动态加载的语言,Java中的类在需要时才会被加载。也就是说,我们编写出的Java程序,在它们开始运行之前并非被完全加载到内存的,其各个部分是在需要时才加载。因此,在我们需要用到某个类时,类加载器首先检查这个类的Class对象是否已被加载,如果没有加载,默认的类加载器就会先根据类名查找.class文件。在这个类的字节码文件被加载时,它们要接受验证,以确保其没有被破坏、并且不包含不良Java代码(这是java众多安全检测机制中的一个),检测通过后Class对象就被载入内存了,可以被用来创建这个类的所有实例对象。下图表示了一个类加载的过程:
-
第一阶段(加载):类加载器根据类名找到此类的.Class文件,并把这个文件包含的字节码加载到内存中,生成Class对象。
-
第二阶段(链接):又分为三个步骤,分别是:
(1) 验证阶段:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(2) 准备阶段:正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
(3)解析阶段:虚拟机将常量池内的符号引用替换为直接引用的过程。
-
第三阶段(初始化):类中静态属性和初始化赋值,以及静态块的执行。
Class对象的获取方式
Java主要提供了三种方式来获取一个实例对象对应的Class对象:
- Class.forName():
这个方法是Class类的一个static成员方法。Class对象和其他对象一样,我们可以获取并操作它的引用,forName()就是取得Class对象的引用的一种方法,该方法允许我们无需通过持有该类的实例对象引用而去获取Class对象。
try {
//"com.yang"是包名
Class c1 = Class.forName("com.yang.Dog");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
注意:如果Class.forName()没有找到你要加载的类,会抛出ClassNotFoundException异常。因此,在调用forName()方法时,需要向上面一样,给出一个ClassNotFoundException异常捕获。
- getClass():
通过new一个对象,用这个对象调用getClass()方法来获取Class的引用。这个方法属于根类Object的一部分,将返回表示该对象类型的Class引用。
Dog dog = new Dog();
Class c2 = dog.getClass();
- 类字面常量:
//字面常量的方式获取Class对象
Class c3 = Dog.class;
用类字面常量的方式来生成Class对象的引用,在编译时会受到检查(因此不需要置于try语句块中来捕获异常),相对前面两种方式显得更简单、更安全。
采用字面常量的方式不仅可以应用于普通的类,也可以应用在接口、数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助,关于反射技术稍后会分析。另外,因为基本数据类型有着对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下:
Class对象 | TYPE字段 |
---|---|
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对象时,触发的是加载阶段,并不会触发最后阶段类的初始化,下面引用《Java编程思想》中的例子来说明这点:
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("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
根据运行结果可以看出:
- 初始化有效地实现了尽可能的“惰性”,通过.Class语法来获取Initable类的Class对象时没有触发初始化,通过Class.forName()方式来获取Initable3类的Class对象时就进行了初始化。
- 调用Initable.staticFinal变量时,只输出了“47”,并没有打印“Initializing Initable”,说明也没有触发初始化,这是因为staticFinal值是“编译期静态常量”,在编译时其值“47”存储到了NotInitialization常量池中,对常量Initable.staticFinal的引用实际都被转化为NotInitialization类对自身常量池的引用了。如果将一个域只设置为static或final,如Initable2.staticNonFinal,还是会触发初始化。