java基础:深入理解Class对象与反射机制 | 数盟

深入理解Class对象

RRIT及Class对象的概念

RRIT(Run-Time Type Identification)运行时类型识别。在《Thinking in Java》一书第十四章中有提到,其作用是在运行时识别一个对象的类型和类的信息。主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。

类是程序的一部分,每个类都有一个class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。所有的类都是在对其第一次使用时,动态加载到JVM中的。例如我们写了一个Test类,编译后生成了Test.class,此时我们的Test类的Class对象就保存在class文件中。当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器子系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。需要特别注意的是,手动编写的每个class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象。

如上所示,实际上JVM内存中只存有一个Test的Class对象。

Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class ans enum)或接口(interface and annotation) (每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取class对象) 。数组同样也被映射为为class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为 class 对象。

到这我们也就可以得出以下几点信息:

Class类也是类的一种,与class关键字是不一样的。

手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件)

每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。

Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载

Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)。

Class对象的加载及获取

Class对象的加载

前面我们已提到过,Class对象是由JVM加载的,那么其加载时机是?实际上所有的类都是在对其第一次使用时动态加载到JVM中的,当程序创建第一个对类的静态成员引用时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件),注意,使用new操作符创建类的新实例对象也会被当作对类的静态成员的引用(构造函数也是类的静态方法),由此看来Java程序在它们开始运行之前并非被完全加载到内存的,其各个部分是按需加载,所以在使用该类时,类加载器首先会检查这个类的Class对象是否已被加载(类的实例对象创建时依据Class对象中类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找.class文件(编译后Class对象被保存在同名的.class文件中),在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良Java代码(这是java的安全机制检测),完全没有问题后就会被动态加载到内存中,此时相当于Class对象也就被载入内存了(毕竟.class字节码文件保存的就是Class对象),同时也就可以被用来创建这个类的所有实例对象。

类加载的过程 :

1. 加载

在加载阶段,虚拟机需要完成3件事:

(1)通过一个类的全限定名(org/fenixsoft/clazz/TestClass)获取定义此类的二进制字节流(.class文件);

(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

(3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口;

2. 验证

验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。验证阶段大致上完成下面4个阶段的验证动作:

(1)文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理;

这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证,字节流才会进入内存的方法区进行储存,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

(2)元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,保证不存在不符合Java语言规范的元数据信息;

(3)字节码验证

通过数据流和控制流分析,确定程序是语义是合法的、符合逻辑的,保证被校验的方法在运行时不会做出危害虚拟机安全的事件;

(4)符号引用验证

可以看作是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行;

3. 准备

准备阶段是正式为类变量分配内存并设置类变量初始值阶段,这些变量所使用的内存都将在方法区中进行分配。这里进行内存分配仅仅是类变量(被static修饰的变量),而不包括实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中;

4. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用进行;

5. 初始化

初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化是如何被触发的:

(1)遇到new、getstatic、putstatic或involestatic这4条指令时;

(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候;
(3)初始化一个类时,如果父类还没被初始化,则先触发父类的初始化;

(4)虚拟机启动时,用户需要指定一个要执行的主类 (包含main()方法的那个类),虚拟机会先初始化这个主类;

(5)如果一个 java.lang.invoke.MethodHandle 实例最后解析的结果是 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,若句柄所对应的类没有进行过初始化,则将它初始化;

上文源自《深入理解java虚拟机》一书,大家可以去读一下,这本书基本上是java程序猿学习必读之一了。在此就不深入展开了,因为这又是另一个JVM领域了。 以后如果写了该方面的文章,会贴到这里。

Class对象的获取

Class对象的获取主要有3种:

通过实例getClass()方法获取

Class.forName方法获取

类字面常量获取

通过实例getClass()方法获取

getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用。

Class.forName方法获取

forName方法是Class类的一个static成员方法,记住所有的Class对象都源于这个Class类,因此Class类中定义的方法将适应所有Class对象。这里通过forName方法,我们可以获取到Test类对应的Class对象引用。 注意调用forName方法时需要捕获一个名称为ClassNotFoundException的异常,因为forName方法在编译器是无法检测到其传递的字符串对应的类是否存在的(是否有.class文件),只能在程序运行时进行检查,如果不存在就会抛出ClassNotFoundException异常。

使用forName方式会触发类的初始化,与之相比的是使用类字面常量获取

类字面常量获取

这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且它根除了对forName()方法的调用,所以也更高效。 注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象,为了使用类而做的准备工作实际包含三个步骤:

加载,这是由类加载器执行的,该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建一个Class对象。

链接。在链接阶段将验证类中的字节码,为静态域分布存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。

初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

如果一个static final值是 编译器常量 ,就像Initable.staticFinal那样,那么这个值不需要对Initable类进行初始化就可以被读取。但是,如果只是将一个域设置为static和final的,还不足以确保这种行为,例如,对Initable.staticFinal2的访问将强制进行类的初始化,因为它不是一个编译期常量。

如果一个static域不是final的,那么在对它访问时,总是要求在它被读取之前, 要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间) ,就像在对Initable2.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对象,肯定会触发初始化,这点在前面已分析过。

instanceof与Class的等价性

关于instanceof 关键字,它返回一个boolean类型的值,意在告诉我们对象是不是某个特定的类型实例。如下,在强制转换前利用instanceof检测obj是不是Animal类型的实例对象,如果返回true再进行类型转换,这样可以避免抛出类型转换的异常(ClassCastException)

而isInstance方法则是Class类中的一个Native方法,也是用于判断对象类型的,看个简单例子:

事实上instanceOf 与isInstance方法产生的结果是相同的。

反射

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为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相关的主要方法如下:

方法返回值 方法名称 方法说明
static Class<?> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象。
Constructor getConstructor(Class<?>… parameterTypes) 返回指定参数类型、具有public访问权限的构造函数对象
Constructor<?>[] getConstructors() 返回所有具有public访问权限的构造函数的Constructor对象数组
Constructor getDeclaredConstructor(Class<?>… parameterTypes) 返回指定参数类型、所有声明的(包括private)构造函数对象
Constructor<?>[] getDeclaredConstructor() 返回所有声明的(包括private)构造函数对象
T newInstance() 调用无参构造器创建此 Class 对象所表示的类的一个新实例。

下面看一个简单例子来了解Constructor对象的使用:

关于Constructor类本身一些常用方法如下(仅部分,其他可查API)

方法返回值 方法名称 方法说明
Class getDeclaringClass() 返回 Class 对象,该对象表示声明由此 Constructor 对象表示的构造方法的类,其实就是返回真实类型(不包含参数)
Type[] getGenericParameterTypes() 按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造函数的形参类型。
String getName() 以字符串形式返回此构造方法的名称。
Class<?>[] getParameterTypes() 按照声明顺序返回一组 Class 对象,即返回Constructor 对象所表示构造方法的形参类型
T newInstance(Object… initargs) 使用此 Constructor对象表示的构造函数来创建新实例
String toGenericString() 返回描述此 Constructor 的字符串,其中包括类型参数。

代码演示如下:

Field类及其用法

Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。同样的道理,我们可以通过Class类的提供的方法来获取代表字段信息的Field对象,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修饰的的字段,无法获取父类的私有字段。下面将通过Field类本身的方法对指定类属性赋值,代码演示如下:

其中的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()等等方法,这里就不全部列出了,需要时查API文档即可。需要特别注意的是被final关键字修饰的Field字段是安全的,在运行时可以接收任何修改,但最终其实际值是不会发生改变的。

Method类及其用法

Method 提供关于类或接口上单独某个方法(以及如何访问该方法)的信息,所反映的方法可能是类方法或实例方法(包括抽象方法)。下面是Class类获取Method对象相关的方法:

方法返回值 方法名称 方法说明
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[] getDeclaredMethod() 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
Method getMethod(String name, Class<?>… parameterTypes) 返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
Method[] getMethods() 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。

同样通过案例演示上述方法:

在通过getMethods方法获取Method对象时,会把父类的方法也获取到,如上的输出结果,把Object类的方法都打印出来了。而getDeclaredMethod/getDeclaredMethods方法都只能获取当前类的方法。我们在使用时根据情况选择即可。下面将演示通过Method对象调用指定类的方法:

在上述代码中调用方法,使用了Method类的invoke(Object obj,Object… args)第一个参数代表调用的对象,第二个参数传递的调用方法的参数。这样就完成了类方法的动态调用。

方法返回值 方法名称 方法说明
Object invoke(Object obj, Object… args) 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Class<?> getReturnType() 返回一个 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就是一个接口而已,在Java8中新增一个默认的方法实现,返回的就参数类型信息

而getParameterTypes/getGenericParameterTypes也是同样的道理,都是获取Method对象所表示的方法的参数类型,其他方法与前面的Field和Constructor是类似的。

文章来源:网络

注:转载文章均来自于公开网络,仅供学习使用,不会用于任何商业用途,如果侵犯到原作者的权益,请您与我们联系删除或者授权事宜,联系邮箱:contact@dataunion.org。转载数盟网站文章请注明原文章作者,否则产生的任何版权纠纷与数盟无关。
期待你一针见血的评论,Come on!

不用想啦,马上 "登录"  发表自已的想法.