JAVA的反射机制是指在运行状态中,对于任意一个类都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。反射是Java语言中非常重要机制,很多第三方框架都用到了反射,本小节将详细讲解反射机制的原理和作用。
19.3.1获得Class类对象
每个类被加载之后,虚拟机就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到这个类。在Java程序中获得Class对象通常有如下三种方式:
- 使用Class类的forName()静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名,全限定类名包含完整的包名和类名。
- 调用某个类的class属性来获取该类对应的Class对象。例如,Person.class 将会返回Person类对应的Class对象。
- 调用某个对象的getClass()方法。该方法是Object 类中的一个方法,所以所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。
第一种方式和第二种方式都是直接根据类来取得该类的Class对象。相比之下,第二种方式有如下两种优势。
- 代码更安全。程序在编译阶段就可以检查需要访问的Class对象是否存在。
- 程序性能更好。因为这种方式无须调用方法,所以性能更好。
也就是说,大部分时候都应该使用第二种方式来获取指定类的Class对象。但如果程序只能获得一个字符串,例如“java.lang.String”,如果需要根据字符串来获取对应的Class对象,则只能使用第一种方式,即使用Class的forName()静态方法获取Class对象,该方法可能抛出ClassNotFoundException异常。一旦获得了某个类所对应的Class对象之后,程序就可以调用Class对象的方法来获得该对象和该类的真实信息了。
19.3.2从Class类对象中获取类的信息
Class类代表了一个类,而一个类的方法、属性以及所在的包也都可以用类来表示。表示普通方法的类是Method,表示构造方法的类是Constructor,表示属性的类是Field,而表示包的类是Package,只有了解了这些类的意义才能很好的学习Class类。
Class类提供了大量方法来获取该Class对象所对应类的详细信息,这些方法中很多都包括多个重载的版本,由于重载版本众多,下面的表19-2对每个方法只列出一个版本,读者可以查阅API文档来查看每个方法的详情。
表19-2 Class类的方法
方法 |
功能 |
Connstructor<T> getConstructor(Class<?>... parameterTypes) |
返回此Class对象对应类的、带指定形参列表的public构造方法。 |
Constructor<?>[] getConstructors() |
返回此Class对象对应类的所有public构造方法 |
Constructor<T> getDeclaredConstructor(Class <?>... parameterTypes) |
返回此Class对象对应类的、带指定形参列表的构造方法,与构造方法的访问权限无关。 |
Constructor<?>[] getDeclaredConstructors() |
返回此Class对象对应类的所有构造方法,与构造方法的访问权限无关 |
Method getMethod(String name, Class<?> ... parameterTypes) |
返回此Class对象对应类的、带指定形参列表的public方法 |
Method[] getMethods() |
返回此Class对象所表示的类的所有public方法 |
Method getDeclaredMethod(String name, Class<?>.. parameterTypes) |
返回此Class对象对应类的、带指定形参列表的方法,与方法的访问权限无关 |
Method[] getDeclaredMethods() |
返回此Class对象对应类的全部方法,与方法的访问权限无关 |
Field getField(String name) |
返回此Class对象对应类的、指定名称的public属性 |
Field[]getFields() |
返回此Class对象对应类的所有public属性 |
Field getDeclaredField(String name): |
返回此Class对象对应类的、指定名称的属性,与属性的访问权限无关 |
Field[] getDeclaredFields(): |
返回此Class对象对应类的全部属性,与属性的访问权限无关 |
<A extends Annotation> A getAnnotation(Class<A> annotationClass); |
尝试获取该Class对象对应类上存在的、指定类型的注解,如果该类型的注解不存在,则返回null |
<A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass) |
该方法尝试获取直接修饰该Class对象对应类的、指定类型的注解,如果该类型的注解不存在,则返回null |
Annotation[] getAnnotations() |
返回修饰该Class对象对应类上存在的所有的注解 |
Annotation[] getDeclaredAnnotations() |
返回直接修饰该Class对应类的所有注解 |
<A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass) |
该方法的功能与getAnnotation()方法基本相似。但由于Java8增加了重复注解功能,因此需要使用该方法获取修饰该类的、指定类型的多个注解 |
Class<?>[] getDeclaredClasses() |
返回该Class对象对应类里包含的全部内部类 |
Class<?> getDeclaringClass() |
返回该Class对象对应类所在的外部类 |
Class<?>[] getlnterfaces() |
返回该Class对象对应类所实现的全部接口 |
Class<? super T> getSuperclass() |
返回该Class对象对应类的父类的Class对象 |
int getModifiers() |
返回此类或接口的所有修饰符。修饰符由public、 protected、private、final、static、abstract 等对应的常量组成,返回的整数应使用Modifer工具类的方法来解码,才可以获取真实的修饰符 |
Package getPackage() |
获取此类的包 |
String getName() |
以字符串 形式返回此Class对象所表示的类的名称 |
String getSimpleName() |
以字符串形式返回此Class对象所表示的类的简称,即不包含包名,只有类名 |
boolean isAnnotation()。 : 。 |
返回此Class对象是否表示一个注解类型 |
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) |
判断此Class 对象是否使用了Annotation修饰 |
boolean isAnonymousClass() |
返回此Class对象是否是一个匿 名类 |
boolean isArray() |
返回此Class对象是否表示一个数组类 |
boolean isEnum() |
返回此Class对象是否表示一个枚举 |
boolean isInterface |
返回此Class对象是否表示一个接口 |
boolean islnstance(Object obj) |
判断obj是否是此Class 对象的实例,该方法与 instanceof操作符作用相同 |
表19-2中,getMethod0方法和getConstructor()方法都需要传入多个类型为Class<?>的参数,这些参数用于获取指定的方法或指定的构造方法。假设某个类内包含如下三个版本的info()方法:
- public void info()
- public void info(String str)
- public void info(String str , Integer num)
这三个同名方法属于重载,它们的方法名相同,但参数列表不同。在Java语言中要确定一个方法
光有方法名是不行的,如果仅仅只指定info()方法,实际上可以是上面三个方法中的任意一一个。如果需要确定一个方法,则应该由方法名和参数列表来确定,但参数名没有任何实际意义,所以只能由参数类型来确定。例如想指定第二个info()方法,则必须指定方法名为info,形参列表为String.class。因此在程序中获取第二个info()方法使用如下代码
如果需要获取第三个info()方法,则使用如下代码:
获取构造方法时无须传入构造方法名称,因为同一个类的所有构造方法的名字都是相同的,所以要确定一个构造方法只要指定参数列表即可。下面的【例19_06】展示了如何通过Class类对象获得类的信息。
【例19_06使用Class类对象获取类的信息】
Exam19_06.java
【例19_06】中Annotation表示注解,关于注解的知识将在19.4小节讲解。【例19_06】的运行结果如图19-6所示。
图19-6【例19_06】运行结果
从图19-6可以看出:getMethods()方法不仅仅能够获得这个类中定义的方法,还可以获得这个类从父类中继承过来的方法。需要注意:虽然定义ClassTest类时使用了@SuppressWarnings注解,但程序运行时无法分析出该类里包含的该注解,这是因为@SuppressWarmings使用了@Retention(value=SOURCE)修饰,这表明@SuppesWarnings只能保存在源代码级别上,而通过ClassTest.class获取该类的运行时Class对象,所以程序无法访问到@SuppressWarnings注解。
19.3.3方法参数反射
Java 8在java.lang.reflect包下新增了一个Executable抽象类,这个类代表所有方法,它有两个子类,分别是代表构造方法的Constructor和代表普通方法的Method。Executable类提供了大量方法来获取修饰该方法的注解信息,还提供了isVarArgs()方法用于判断该方法是否包含数量可变的形式参数,以及通过getModifiers()方法来获取该方法的修饰符。除此之外,Executable提供了如表19-3所示的两个方法来获取该方法的参数个数及参数名称。
表19-3 Executable类获取参数个数及参数名称的方法
方法 |
功能 |
int getParameterCount() |
获取该方法的参数个数 |
Parameter[] getParameters() |
获取该方法的所有参数 |
表19-3中,Parameter类就表示方法的形式参数。Parameter也提供了大量方法来获取声明该参数的各项信息,如表19-4所示。
表19-4 Parameter类的常用方法
方法 |
功能 |
getModifiers() |
获取修饰该参数的修饰符 |
String getName() |
获取参数名称 |
Type getParameterizedType() |
获取带泛型的参数类型 |
Class<?> getType() |
获取参数类型 |
boolean isNamePresent() |
判断该方法返回该类的class文件中是否包含了方法的参数名称信息。 |
boolean isVarArgs() |
判断该参数是否为个数可变的形参 |
需要指出:IDEA编译Java 源文件时,默认生成的class文件并不包含方法的参数名称信息,因此调用isNamePresent()方法将会返回false,调用getName0方法也不能得到该参数的形参名。如果希望编译Java源文件时可以保留参数名称信息,需要对IDEA进行编译参数的设置,具体步骤是:以打开“File”菜单,选择“Settings”菜单项,在弹出的对话框中按照“Build,Execution,Deployment”->“Compiler”->“Java Compiler”的顺序选择选项,并在“Additional command line parameters”后面填上“-parameters”,如图19-7所示。
图19-7 设置编译参数
下面的【例19_07】展示了方法参数反射实现过程。
【例19_07方法参数反射】
Exam19_07.java
如果希望能够正确运行【例19_07】,就必须对IDEA设置编译参数,设置完成后还需重新编译项目,具体做法是:打开“菜单”,单击“Rebuild Project”菜单项。完成这一步操作后,运行【例19_07】的结果如图19-8所示。
图19-8【例19_07】运行结果
19.3.4利用反射生成并操作对象
一个Class类对象就代表一个类,程序员不仅仅能够通过这个Class类对象获得对应类的信息,还能通过这个Class类对象创建出对应类的对象。这句话听起来有点绕口,它的意思是:如果一个Class类对象clazz代表A类,那么程序员能够通过clazz来创建出一个A类对象。19.3.3小节曾介绍过:Constructor类代表类的构造方法,而通过反射的方式来生成对象需要先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的对象。下面的【例19_08】展示了使用反射方式创建对象的过程。
【例19_08 用反射技术创建对象1】
Exam19_08.java
【例19_08】中定义了一个ObjectPoolFactory类,这个类的createObject()方法可以根据参数指定的类名称通过反射方式创建出该类的对象,而initPool()根据一组类的名称生成一组对象并存入Map集合中。main()方法创建了ObjectPoolFactory类对象并调用其initPool()方法生成一组对象并循环打印这些对象。【例19_08】的运行结果如图19-9所示。
图19-9【例19_08】运行结果
需要注意:利用反射方式生成的第二个对象是Date类对象,而Date类无参数构造方法所创建的对象表示当前时间,因此读者的运行结果中Date类对象的打印结果可能与图19-9不同。
【例19_08】中调用的是类的无参数构造方法创建的对象,如果希望调用有参数的构造方法创建对象,则需要调用有参数的getConstructor()方法来获得Constructor对象。有参数的getConstructor()方法的参数类型是Class,这个参数表示对应类构造方法的参数,而获得的Constructor对象则是对应类的带参数构造方法。例如A类有一个带有参数的构造方法,这个构造方法的参数是String类型,那么通过Class类的有参数的getConstructor()就能获得这个有参数的构造方法,在调用getConstructor()时需要以String.class作为getConstructor()方法的参数。当获得了表示有参数构造方法的Constructor对象后,再调用其newInstance()方法就能以A类有参数的构造方法创建出一个A类对象,并且在调用newInstance()方法时需要向它传递相应的构造方法参数。下面的【例19_09】展示了如何用反射的方式调用到java.util.Date类的有long型参数的构造方法。
【例19_09 用反射技术创建对象2】
Exam19_09.java
【例19_09】中,语句①调用了有参数的getConstructor()方法来获得Date类有参数的构造方法,由于为getConstructor()方法传递的参数是“long.class”,因此获得的Constructor对象表示的是“Date(long date)”这个构造方法。同样,在调用Constructor对象的newInstance()方法时,也需要传递一个long型的参数,这相当于为“Date(long date)”这个构造方法传递了构造参数。此外还需强调,Java语言中,基础数据类型也可以通过“.class”来获得其对应类型的Class对象,并且“long.class”与“Long.class”所得到的Class对象并不是同一个对象。【例19_09】的运行结果如图19-10所示。
图19-10【例19_09】运行结果
通过反射的方式不仅能创建出一个类的对象,还能调用到一个类中的方法。如果想通过反射方式调用一个类的方法,先要调用Class的getMethods()方法或getMethod()方法来获取全部方法或指定方法,这两个方法的返回值是Method数组和Method对象。前文介绍过:每个Method对象对应一个方法,获得Method对象后,程序就可通过该Method来调用它对应的方法。调用的具体方式是:在程序中执行Method类的invoke()方法,需要指出:invoke()方法在执行时需要一个对象作为参数,执行invoke()方法就相当于调用了这个对象相应的方法。下面的【例19_10】展示了使用反射方式调用类方法的过程。
【例19_10用反射技术调用类中的方法】
Exam19_10.java
【例19_10】的语句①通过Method对象的invoke()方法调用了System.out的println()方法,而“这是通过反射方式打印的字符串”则是传递给println()方法的参数。【例19_10】的运行结果如图19-11所示。
图19-11【例19_10】运行结果
通过反射的方式还可以访问类的属性。程序员只需要通过Class类的getFields()或getField()方法可以获取该类所包括的全部属性或指定的属性。Java语言中,属性由Field类表示,Field类提供了一组getXxx()方法来获取对象的某个属性的值,而此处的Xxx对应8种基础数据类型,如果该属性的类型是引用类型,则取消get后面的Xxx。此外,Field类还提供了一组setXxx()方法为对象的属性设置成值,此处的Xxx也对应8种基础数据类型,同样如果该属性的类型是引用类型则取消set后面的Xxx。下面的【例19_11】演示了如何使用反射技术操作属性。
【例19_11用反射技术访问属性】
Exam19_11.java
【例19_11】中定义了一个Person类,该类里包含两个private属性:name和age,在通常情况下,这两个属性只能在Person 类里访问。但main()方法中通过反射修改了Person对象的name、age两个属性的值。语句①使用getDeclaredField()方法获取了名为name 的属性,注意此处不是使用getField()方法,因为getField()方法只能获取public的属性,而getDeclaredField()方法则可以获取所有的属性。语句②是设置访问该属性时不受访问权限的控制,语句③修改了Person对象的name属性的值。修改Person对象的age属性值的方式与修改name属性的方式完全相同。【例19_11】的运行结果如图19-12所示。
图19-12【例19_11】运行结果
19.3.5利用反射操作数组
在Java语言中,数组也可以用反射的方式生成并进行操作。在java.lang.reflect包下提供了一个Array类,Array对象可以代表所有的数组。程序员可以通过使用Array类动态地创建数组,操作数组元素等。Array类所提供的操作数组常用方法如表19-5所示。
表19-5 Array类操作数组的方法
方法 |
功能 |
static Object newInstance(Class<?> componentType, int... length) |
创建一个具有指定的元素类型、指定维度的新数组 |
static xxx getXxx(Object array, int index) |
返回array 数组中第index个元素。其中xxx是各种基础数据类型,如果数组元素是引用类型,则该方法变为get(Object array, int index) |
static void setXxx(Object array, int index, xxx val) |
将array数组中第index 个元素的值设为val,其中xxx是各种基础数据类型,如果数组元素是引用类型,则该方法变成set(Object array, intindex, Object val) |
下面的【例19_12】展示了使用Array类创建并操作数组的过程。
【例19_12 Array类操作数组1】
Exam19_12.java
【例19_12】的运行结果如图19-13所示。
图19-13【例19_12】运行结果
【例19_12】创建并操作了一个一维数组,而Array实际上也能创建并操作多维数组,下面的【例19_13】展示了如何使用Array类创建并操作三维数组。在3.2小节曾经介绍过:多维数组实际上也是一维数组,例如三维数组可以看作是由多个二维数组组成的一维数组,因此可以用操作一维数组的方式来操作二维、三维或更高维度的数组。
【例19_13 Array类操作数组2】
Exam19_13.java
【例19_13】创建并操作了一个三维数组,操作的每一步都添加了详细的注释,读者可以根据注释理解每一步的操作意义。【例19_13】的运行结果如图19-14所示。
图19-14【例19_13】运行结果
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。