一、理解Class对象
(一)RRIT的概念以及Class对象的作用
认识Class对象之前,先来了解一个概念,RRIT(Run-Time Type Identification)运行时类型识别。Java中出现RRIT的说法是源于《Thinking in Java》一书,其作用是在运行时识别一个对象的类型和类的信息,这里分为两种:传统的“RRIT”,它假定我们在编译期已知道了所有类型(在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如new对象时该类必须已定义好),另外一种是反射机制,它允许我们在运行时发现和使用类型的信息。在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。
Class类被创建后的对象就是Class对象,注意,Class对象表示的是自己手动编写类的类型信息,比如创建一个Shape类,那么JVM就会创建一个Shape对应Class类的Class对象,该Class对象保存了Shape类相关的类型信息。实际上Java中每个类都有一个Class对象,每当我们编写并且编译一个新创建的类就会产生一个对应的Class对象并且这个Class对象会被保存在同名.class文件里(编译后的字节码文件保存的就是Class对象),那为什么需要这样一个Class对象呢?
是这样的,当我们new一个新对象或者引用静态成员变量时,java虚拟机(JVM)中的类加载器子系统会将对应的Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要的实例对象或者提供静态变量的引用值。需要特别注意的是,手动编写的每一个Class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个Class对象。我们可以得出以下几点信息:
(1)Class类也是类的一种,与class关键字是不一样的。
(2)手动编写的类被编译后会产生一个Class对象,其表示的是创建类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件),比如创建一个Shape类,编译后就会创建包含Shape类相关类型信息的Class对象,并保存在Shape.class字节码文件中。
(3)每个通过关键字class标志的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
(4)Class类只存私有构造函数,因此对应Class对象只能由JVM创建和加载。
(5)Class类的对象其作用是运行时提供或者获得某个对象的类型信息,这点对于反射技术很重要。
二、Class对象的加载及获取方式
(一)Class对象的加载
所有的类都是在对其第一次使用时动态加载到JVM中的,当程序创建第一个对类的静态成员引用时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件),注意,使用new操作符创建类的新实例对象也会被当作对类的静态成员变量的引用(构造函数也是类的静态方法),由此看来,java程序在它们开始运行之前并非完全加载到内存的,其各个部分是按需加载,所以在使用该类时,类加载器首先会检查这个类的Class对象是否已经被加载(类的实例对象创建是根据Class对象中的类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找.class文件,在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良java代码(这是java的安全机制检测),完全没有问题后就会被载入内存了,同时也就可以被用来创建这个类的所有实例对象。
(二)Class.forName方法
Class.forName()的调用将会返回一个对应类的Class对象,因此如果我们想获取一个类的运行时类型信息并加以使用时,可以调用Class.forName()获取 Class对象的使用,这样做的好处是无需通过持有该类的实例对象的引用而去获取Class对象。调用该方法时需要捕获一个ClassNotFoundException异常,因为forName方法在编译器是无法检测到其传递的字符串对应的类是否存在的,只能在程序运行时进行检查,如果不存在就抛出该异常。
(三)Class字面常量
在java中存在另一种方式来生成Class对象的引用,它就是Class字面常量,如:Class clazz = Gum.class。
这种方法相对于前面两种方法更加简单,更安全。因为它在编译期就会受到编译器的检查,同时由于无需调用forName方法,效率也会更高,因为通过字面量的方法获取Class对象的引用不会自动初始化该类。更加有趣的是字面常量的获取Class对象的引用方式不仅可以应用于普通的类,也可以应用于接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助。由于基本数据类型还有对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下,一般情况下更倾向使用.class的形式,这样可以保持与普通类的形式统一。
boolean.class = Boolean.Type;
使用字面常量的方式获取Class对象的引用不会触发类的初始化,这里我们需要简单了解一下类加载的过程:
加载——》验证——》准备——》解析——》初始化
(1)加载:类加载过程中的第一个阶段,通过一个类的完全路径查找此类字节码文件,并利用字节码文件创建一个 Class对象。
(2)验证:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。
(3)初始化:类加载的最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。
由此可知,我们通过字面常量获取Class引用时,触发的应该是加载阶段,因为在这个阶段Class对象一创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化。
总结:
(1)获取Class对象引用的方式有3种,通过继承自Object类的getClass方法,Class类的静态方法forName以及字面常量的方式。
(2)其中实例类的getClass方法和Class类的静态方法forName都将触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化。
(3)初始化是类加载的最后一个阶段,也就是说完成这个阶段后也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的java程序代码或者字节码。
关于类加载的初始化阶段,在虚拟机规范严格规定了有且只有4中场景必须对类进行初始化:
(1)使用new关键字实例化对象时、读取或设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段)。
(2)使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。
(3)当初始化一个类的时候,如果其父类还没进行初始化则先需触发其父类的初始化。
(4)当java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。
(四)理解泛化的Class对象引用
由于Class的引用总是指向某个类的Class对象,利用Class对象可以创建实例类,这也足以说明Class对象的引用指向的对象确切的类型。在java SE5引入泛型后,使我们可以利用泛型来表示Class对象更具体的类型,即使运行期间会被擦除,但编译期足以确保我们使用正确的对象类型。
声明普通的Class对象,在编译其并不会检查Class对象的确切类型是否符合要求,如果存在错误只会在运行期才得以暴露出来。但是通过泛型声明指明类型的Class对象,编译器在编译期将对带泛型的类进行额外的类型检查,确保其在编译期就能保证类型的正确性。向Class引用添加泛型约束仅仅是为了提供编译期类型的检查从而避免将错误延续到运行时期。
(五)instanceof 关键字与isInstance方法
关于instanceof 关键字,它返回一个boolean类型的值,意在告诉我们对象是不是某个特定的类型实例。如下,在强制转换前利用instanceof检测obj是不是Animal类型的实例对象,如果返回true再进行类型转换,这样可以避免抛出类型转换的异常(ClassCastException)。
public void cast2(Object obj){
if(obj instanceof Animal){
Animal animal= (Animal) obj;
}
}
事实上instanceOf 与isInstance方法产生的结果是相同的。对于instanceOf是关键字只被用于对象引用变量,检查左边对象是不是右边类或接口的实例化。如果被测对象是null值,则测试结果总是false。一般形式:
//判断这个对象是不是这种类型
obj.instanceof(class)
而isInstance方法则是Class类的Native方法,其中obj是被测试的对象或者变量,如果obj是调用这个方法的class或接口的实例,则返回true。如果被检测的对象是null或者基本类型,那么返回值是false;一般形式如下:
//判断这个对象能不能被转化为这个类
class.isInstance(obj)
三、理解反射技术
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。一直以来反射技术都是java中的闪亮点,这也是目前大部分框架(如Spring/Mybatis等)得以实现的支柱。
在java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类,表示的是Class对象所表示的类的构造方法,利用它可以在运行时动态创建对象;Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private);Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private)。
(一)Constructor类及其用法
Constructor存在于反射包(java.lang.reflect)中,反映的是Class 对象所表示的类的构造方法。获取Constructor是通过Class类中的方法获取的,Class类与Constructor相关的主要方法如下:
Constructor类存在于反射包(java.lang.reflect)中,反映的是Class 对象所表示的类的构造方法。获取Constructor对象是通过Class类中的方法获取的,Class类与Constructor相关的主要方法如下:
方法返回值 方法名称 方法说明
static Class> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象
Constructor
Constructor>[] getConstructors() 返回所有具有public访问权限的构造函数的Constructor对象数组
Constructor
Constructor>[] getDeclaredConstructor() 返回所有声明的(包括private)构造函数对象
T newInstance() 创建此 Class 对象所表示的类的一个新实例。
关于Constructor类本身一些常用方法如下(仅部分,其他可查API),
方法返回值 方法名称 方法说明
Class
Type[] getGenericParameterTypes() 按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造 函数的形参类型。
String getName() 以字符串形式返回此构造方法的名称。
Class>[] getParameterTypes() 按照声明顺序返回一组 Class 对象,即返回Constructor 对象所表示构 造方法的形参类型
T newInstance(Object... initargs) 使用此 Constructor对象表示的构造函数来创建新实例
String toGenericString() 返回描述此 Constructor 的字符串,其中包括类型参数。
其中关于Type类型这里简单说明一下,Type是java编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。getGenericParameterTypes 与 getParameterTypes都是获取构成函数的参数类型,前者返回的是Type类型,后者返回的是Class类型,由于Type顶级接口,Class也实现了该接口,因此Class类是Type的子类,Type 表示的全部类型而每个Class对象表示一个具体类型的实例,如String.class仅代表String类型。由此看来Type与Class表示类型几乎是相同的,只不过Type表示的范围比Class要广得多而已。当然,Type还有其他子类,如:
TypeVariable:表示类型参数,可以有上界,比如:T extends Number。
ParameterizedType:表示参数化的类型,有原始类型和具体的类型参数,比如:List。
WildcardType:表示通配符类型,比如:?, ? extends Number, ? super Integer。
通过以上的分析,对于Constructor类已有比较清晰的理解,利用好Class类和Constructor类,我们可以在运行时动态创建任意对象,从而突破必须在编译期知道确切类型的障碍。
(二)Field类及其用法
Field提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。同样的道理,我们可以通过Class提供的方法来获取代表字段信息的Filed对象,Class类与Field对象相关方法如下:
方法返回值 方法名称 方法说明
Field getDeclaredField(String name) 获取指定name名称的(包含private修饰的)字段,不包括继承的字段
Field[] getDeclaredField() 获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括 继承的字段
Field getField(String name) 获取指定name名称、具有public修饰的字段,包含继承字段
Field[] getField() 获取修饰符为public的字段,包含继承字段
上述方法需要注意的是,如果我们不期望获取其父类的字段,则需使用Class类的getDeclaredField/getDeclaredFields方法来获取字段即可,倘若需要连带获取到父类的字段,那么请使用Class类的getField/getFields,但是也只能获取到public修饰的的字段,无法获取父类的私有字段。
其中的set(Object obj, Object value)方法是Field类本身的方法,用于设置字段的值,而get(Object obj)则是获取字段的值,当然关于Field类还有其他常用的方法如下:
方法返回值 方法名称 方法说明
void set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
Object get(Object obj) 返回指定对象上此 Field 表示的字段的值。
Class getType() 返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。
boolean isEnumConstant() 如果此字段表示枚举类型的元素则返回 true;否则返回 false。
String toGenericString() 返回一个描述此 Field(包括其一般类型)的字符串。
String getName() 返回此 Field 对象表示的字段的名称。
Class getDeclaringClass() 返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段。
void setAccessible(boolean flag) 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性。
上述方法可能是较为常用的,Field类还提供了专门针对基本数据类型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法。需要特别注意的是被final关键字修饰的Field字段是安全的,在运行时可以接收任何修改,但最终其实际值是不会发生改变的。
(三)Method类及其用法
Method提供关于类或接口上单独某个方法(以及如何访问该方法)的信息,所反映的方法可能是类方法或实例方法(包括抽象方法)。下面是Class类获取Method对象相关的方法:
方法返回值 方法名称 方法说明
Method getDeclaredMethod(String name, Class... parameterTypes) 返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[] getDeclaredMethod() 返回 Method 对象的一个数组,不包括继承的方法。
Method getMethod(String name, Class... parameterTypes) 返回一个 Method 对象,它反映此 Class 对象所表示的类或 接口的指定公共成员方法。
Method[] getMethods() 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明 的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。
在通过getMethods方法获取Method对象时,会把父类的方法也获取到。而getDeclaredMethod/getDeclaredMethods方法都只能获取当前类的方法。
Method类包含的方法如下:
方法返回值 方法名称 方法说明
Object invoke(Object obj, Object... args) 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Classget ReturnType() 返回一个 Class 对象,该对象描述了此Method对象所表示的方法的正式返回类型, 即方法的返回类型。
Type getGenericReturnType() 返回表示由此 Method 对象所表示方法的正式返回类型的 Type 对象,也是方法的返 回类型。
Class[] getParameterTypes() 按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的法 的形参类型。即返回方法的参数类型组成的数组
Type[] getGenericParameterTypes() 按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法 的形参类型的,也是返回方法的参数类型
String getName() 以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称
boolean isVarArgs() 判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true; 否则,返回 false。
String toGenericString() 返回描述此 Method 的字符串,包括类型参数。
getReturnType方法/getGenericReturnType方法都是获取Method对象表示的方法的返回类型,只不过前者返回的Class类型,后者返回的Type(前面已分析过),Type就是一个接口而已。
四、结语
我们应该认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM只会简单地检查这个对象,判断该对象属于哪种类型,同时也应该知道,在使用反射机制创建对象前,必须确保已加载了这个类的Class对象,当然这点完全不必由我们操作,毕竟只能JVM加载,但必须确保该类的”.class”文件已存在并且JVM能够正确找到。关于Class类的方法在前面我们只是分析了主要的一些方法,其实Class类的API方法挺多的,建议查看一下API文档,浏览一遍,有个印象也是不错的选择。
/**
* 修饰符、父类、实现的接口、注解相关
*///获取修饰符,返回值可通过Modifier类进行解读
public native int getModifiers();
//获取父类,如果为Object,父类为null
public native Class getSuperclass();
//对于类,为自己声明实现的所有接口,对于接口,为直接扩展的接口,不包括通过父类间接继承来的
public native Class[] getInterfaces();
//自己声明的注解
public Annotation[] getDeclaredAnnotations();
//所有的注解,包括继承得到的
public Annotation[] getAnnotations();
//获取或检查指定类型的注解,包括继承得到的
public A getAnnotation(Class annotationClass);
public boolean isAnnotationPresent(Class annotationClass);
/**
* 内部类相关
*/
//获取所有的public的内部类和接口,包括从父类继承得到的
public Class[] getClasses();
//获取自己声明的所有的内部类和接口
public Class[] getDeclaredClasses();
//如果当前Class为内部类,获取声明该类的最外部的Class对象
public Class getDeclaringClass();
//如果当前Class为内部类,获取直接包含该类的类
public Class getEnclosingClass();
//如果当前Class为本地类或匿名内部类,返回包含它的方法public Method getEnclosingMethod();
/**
* Class对象类型判断相关
*/
//是否是数组
public native boolean isArray();
//是否是基本类型
public native boolean isPrimitive();
//是否是接口
public native boolean isInterface();
//是否是枚举
public boolean isEnum();
//是否是注解public boolean isAnnotation();
//是否是匿名内部类public boolean isAnonymousClass();
//是否是成员类
public boolean isMemberClass();
//是否是本地类
public boolean isLocalClass();