概述

反射是框架设计的灵魂, 因为它赋予了我们在运行时分析类以及执行类中方法的能力。
反射机制:将类的各个组成部分封装为其他对象

  • 优点
    • 可以在程序运行过程中,操作通过反射机制封装的对象;
    • 解耦,提高程序的可扩展性
  • 缺点:

    • 安全问题,让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时);
    • 性能问题,反射性能稍差,不过,对于框架来说实际是影响不大的。
  • 获取类信息

Java程序中的许多对象在运行时都会出现两种类型:编译时类型和运行时类型。例如:Person p = new Student(); 行代码将会生成一个p变量,该变量的编译时类型为Person。如果程序需要调用该对象运行时类型中的方法,那么我们该怎么做呢? 解决这个问题有如下两个方法:

  • 方法一:假设使用者在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。
  • 方法二:编译时根本无法预知该对象和类可能属于哪些 类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

Class对象

每个类被加载之后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类及其该类的内部信息。Java中获得Class对象通常有如下三种方式:

Way Belong Paraphrase Phase
forName(String clazzName) Class 参数是某个类的全限定类名,是Class类的静态方法。
如:Class.forName(com.srwl.mytx.Person)
Source 源代码阶段(硬盘)
class Class 调用类的class属性来获取该类对应的Class对象。
如:Person.class将会返回Person类对应的Class对象
Class 类对象阶段(内存)
getClass() Object 所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。 Runtime 运行时阶段(内存)

对于第一种方式和第二种方式都是直接根据类来取得该类的Class对象,相比之下,第二种方式有如下两种优势

  1. 代码更安全,程序在编译阶段就可以检查需要访问的Class对象是否存在;
  2. 程序性能更好,因为这种方式无须调用方法,所以性能更好。

Tips:

  • 同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过那种方式获取的Class对象都是同一个;
  • 通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行。

Class对象的方法

通过Class对象可以得到大量的Method、Constructor、Field等对象,这些对象分别代表该类所包括的方法、构造器和成员变量等,程序还可以通过这些对象来执行实际的功能,例如调用方法、创建实例。
Class类提供了大量的实例方法来获取该Class对象所对应类的详细信息,Class类大致包含如下方法,下面每个方法都可能包括多个重载的版本,可查阅API文档来掌握它们。

获取构造器
Connstructor getConstructor(Class<?>…parameterTypes) 返回此Class对象对应类的、带指定形参列表的public构造器。
Constructor 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对象对应类的全部成员变量,与成员变量的访问权限无关。
获取Annotation
A getAnnotation(Class annotationClass) 尝试获取该Class对象对应类上存在的、指定类型的Annotation;如果该类型的注解不存在,则返回null。
A getDeclaredAnnotation(Class annotationClass) 尝试获取修饰该Class对象对应类的、指定类型的Annotation,包括private修饰的;如果该类型的注解不存在,则返回null。
Annotation[] getAnnotations() 返回修饰该Class对象对应类上存在的所有Annotation。
Annotation[] getDeclaredAnnotations() 返回直接修饰该Class对应类的所有Annotation。
A[] getAnnotationsByType(Class annotationClass) 与getAnnotation()类似。但Java 8加了重复注解功能,因此需使用该方法获取修饰该类的、指定类型的多个Annotation
A[] getDeclaredAnnotationsByType(Class annotationClass) 与getDeclaredAnnotations ()类似。但Java 8增加了重复注解功能,因此需要使用该方法获取直接修饰该类的、指定类型的多个Annotation
获取内部类、外部类、实现接口、父类Class对象
Class<?>[] getDeclaredClasses() 返回该Class对象对应类里包含的全部内部类
Class<?> getDeclaringClass() 返回该Class对象对应类所在的外部类
Class<?>[] getInterfaces() 返回该Class对象对应类所实现的全部接口
Class<? super T > getSuperclass() 返回该Class对象对应类的超类的Class对象
获取Class对象对应类的修饰符、所在包、类名等基本信息
int getModifiers() 返回此类或接口的所有修饰符。 注意:修饰符由public、protected、private、final、static、abstract等对应的常量组成,返回的整数应使用Modifier工具类的方法来解码,才可以获取真实的修饰符。
Package getPackage() 获取此类的包
String getName() 以字符串形式返回此Class对象所表示的类的名称
String getSimpleName() 以字符串形式返回此Class对象所表示的类的简称
判断该类是否为接口、枚举、注解类型等
boolean isAnnotation() 返回此Class对象是否表示一个注解类型(由@interface定义)
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass) 判断此Class对象是否使用了Annotation修饰
boolean isAnonymousClass() 返回此Class对象是否是一个匿名类
boolean isArray() 返回此Class对象是否表示一个数组类
boolean isEnum() 返回此Class对象是否表示一个枚举(由enum关键字定义)
boolean isInterface() 返回此Class对象是否表示一个接口(使用interface定义)
boolean isInstance(Object obj) 判断obj是否是此Class对象的实例。该方法可以完全代替instanceof操作符

注意:对于只能在源代码上保留的注解,使用运行时获得的Class对象无法访问到该注解对象。

  • 例如:

虽然定义ClassTest类时使用了@SuppressWarnings注解,但程序运行时无法分析出该类里包含的该注解,这是因为@SuppressWarnings使用了@Retention(value=SOURCE)修饰,这表明@SuppressWarnings只能保存在源代码级别上,而通过ClassTest.class获取该类的运行时Class对象,所以程序无法访问到@SuppressWarnings注解。

方法参数反射

Java 8在java.lang.reflect包下新增了一个Executable抽象基类,该对象代表可执行的类成员,该类派生了Constructor、Method两个子类。

Executable类的方法
int getParameterCount() 获取该构造器或方法的形参个数
Parameter[] getParameters() 获取该构造器或方法的所有形参,每个Parameter对象代表方法或构造器的一个参数
Parameter类的方法
getModifiers() 获取修饰该形参的修饰符
String getName() 获取形参名
Type getParameterizedType() 获取带泛型的形参类型
Class<?> getType() 获取形参类型
boolean isNamePresent() 该方法返回该类的class文件中是否包含了方法的形参名信息
boolean isVarArgs() 用于判断该参数是否为个数可变的形参

需要指出的是,使用javac命令编译Java源文件时,默认生成的class文件并不包含方法的形参名信息,因此调用isNamePresent()方法将会返回false,调用getName()方法也不能得到该参数的形参名。如果希望javac命令编译Java源文件时可以保留形参信息,则需要为该命令指定-parameters选项。

创建对象

通过反射来生成对象需要先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance(Object … initargs)方法来创建该Class对象对应类的实例。
参数:initargs数组代表构造函数的实参列表。

调用方法

  1. 当获得某个类对应的Class对象后,就可以通过该Class对象的getMethods()方法或者getMethod()方法来获取全部方法或指定方法,这两个方法的返回值是Method数组,或者Method对象。<br />每个Method对象对应一个方法,获得Method对象后,程序就可通过该Method来调用它对应的方法。在Method里包含一个invoke()方法,该方法的签名如下:Object invoke(Object obj, Object...args):该方法中,obj:执行该方法的主调; args:执行该方法时传入该方法的实参。

Tips:Spring框架就是通过这种方式将成员变量值以及依赖对象等都放在配置文件中进行管理的,从而实现了较好的解耦。这也是Spring框架的IOC的秘密。

  • 通过反射来取消访问权限检查

当通过Method的invoke()方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限。如果程序确实需要调用某个对象的private方法,则可以先调用Method对象的如下方法:

  • setAccessible(boolean flag):将Method对象的accessible设置为指定的布尔值。值为true,指示该Method在使用时应该取消Java语言的访问权限检查;值为false,则指示该Method在使用时要实施Java语言的访问权限检查。

Tips:
实际上,setAccessible()方法并不属于Method,而是属于它的父类AccessibleObject。因此Method、Constructor、Field都可调用该方法,从而实现通过反射来调用private方法、private构造器和private成员变量。也就是说,它们可以通过调用该方法来取消访问权限检查,通过反射即可访问private成员。

访问成员变量值

  1. 通过Class对象的getFields()或getField()方法可以获取该类所包括的全部成员变量或指定成员变量。Field提供了如下两组方法来读取或设置成员变量值:
  • getXxx(Object obj):获取obj对象的该成员变量的值。此处的Xxx对应8种基本类型,如果该成员变量的类型是引用类型,则取消get后面的Xxx。
  • setXxx(Object obj, Xxx val):将obj对象的该成员变量设置成val值。此处的Xxx对应8种基本类型,如果该成员变量的类型是引用类型,则取消set后面的Xxx。

使用这两个方法可以随意地访问指定对象的所有成员变量,包括private修饰的成员变量。

操作数组

在java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array来动态地创建数组,操作数组元素等。
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,int index,Object val)。

嵌套访问权限

Jdk11新增功能,
在Java 中,外部类可以访问内部类的private成员,内部类之间也可相互访问对方的private成员;但,Java11之前,如果外部类通过反射访问内部类的private成员,或者内部类之间通过反射访问对方的private成员,这就不行了。
为了解决这个问题,Java 11引入了嵌套上下文的概念。通过嵌套访问权限的支持,Java 11统一了通过反射访问和不通过反射访问时的权限不一致的问题。
与之对应的是,Java 11为Class类新增了如下方法:

Class<?> getNestHost() 返回该类所属的嵌套属主
boolean isNestmateOf(Class<?> c) 判断该类是否为c的嵌套同伴(nestmate),只要两个类有相同的嵌套属主,它们就是嵌套同伴
Class<?>[] getNestMembers() 获取该类的所有嵌套成员

Tips:对于外部类而言,它的嵌套属主就是它自身;对于内部类而言,它的嵌套属主就是它所在的外部类。

JDK动态代理

在Java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口可以生成JDK动态代理类或动态代理对象。

How

Proxy提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。
如果在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy来创建动态代理类;
如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。

Proxy提供了如下两个方法来创建动态代理类和动态代理实例:


1. static Class<?> getProxyClass(ClassLoader loader, Class<?>…interfaces)
创建一个动态代理类所对应的Class对象,该代理类将实现interfaces数组中的接口。第一个ClassLoader参数指定生成动态代理类的类加载器。

2. static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
直接创建一个动态代理对象,该代理对象的实现类实现了interfaces数组中的接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法。
注:实际上,即使采用第一个方法生成动态代理类之后,如果程序需要通过该代理类来创建对象,依然需要传入一个InvocationHandler对象。也就是说,系统生成的每个代理对象都有一个与之关联的InvocationHandler对象。
  • InvocationHandler

计算机是很“蠢”的,当程序使用反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口。动态代理对象就需要实现一个或多个接口里定义的所有方法,但问题是:系统怎么知道如何实现这些方法?这个时候就轮到InvocationHandler对象登场了,当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法。

程序中可以采用先生成一个动态代理类,然后通过动态代理类来创建代理对象的方式生成一个动态代理对象。代码片段如下:

  1. //创建一个InvocationHandler对象
  2. InvocationHandler handler = new MyInvocationHandler(...);
  3. //使用Proxy生成一个动态代理类proxyClass
  4. Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(),Foo.class);
  5. //获取proxyClass类中带一个InVocationHandler参数的构造器
  6. Constructor ctor = proxyClass.getConstructor(InvocationHandler.class);
  7. //调用ctor的newInstance方法来创建动态实例
  8. var f = (Foo) ctor.newInstance(handler);
  9. 也可直接生成一个动态代理对象,代码片段如下:
  10. //创建一个InvocationHandler对象
  11. InvocationHandler handler = new MyInvocationHandler(…);
  12. //使用Proxy直接生成一个动态代理对象
  13. var f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.class},handler);
  14. //使用Proxy和InvocationHandler来生成动态代理对象,代码如下:
  15. interface Person{
  16. void walk();
  17. void sayHello(String name);
  18. }
  19. class MyInvokationHandler implements InvocationHandler{
  20. /* 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
  21. proxy:动态代理对象;method:正在执行的方法;args:调用目标方法时传入的实参 */
  22. public Object invoke(Object proxy,Method method,Object[] args){
  23. system.out.println("正在执行方法:"+ method)
  24. if(args == null){
  25. system.out.println(“该方法没有实参")
  26. return null;
  27. }
  28. system.out.println(“该方法有实参:”);
  29. for(var val : args){
  30. system.out.println(val);
  31. }
  32. }
  33. }
  34. public class ProxyTest{
  35. public static void main(String[] args){
  36. //创建一个InvocationHandler对象
  37. InvocationHandler handler = new MyInvocationHandler();
  38. //使用Proxy直接生成一个动态代理对象
  39. var p = (Person) Proxy.newProxyInstance( Person.class.getClassLoader(), new Class[ ]{Person.class}, handler );
  40. //调用动态代理对象的walk()和sayHello()方法
  41. p.walk();
  42. p.sayHello("一野”);
  43. }
  44. }
  45. 控制台
  46. ======================
  47. 正在执行方法:public abstract void Person.walk()
  48. 该方法没有实参
  49. 正在执行方法:public abstract void Person.sayHello(String name)
  50. 该方法有实参:
  51. 一野

上面程序首先提供了一个Person接口,该接口中包含了walk()和sayHello()两个抽象方法,接着定义了一个简单的InvocationHandler实现类,定义该实现类时需要重写invoke()方法,调用代理对象的所有方法时都会被替换成调用该invoke()方法。

看完了上面的示例程序,可能有读者会觉得这个程序没有太大的实用价值,难以理解Java动态代理的魅力。实际上,在普通编程过程中,确实无须使用动态代理,但在编写框架或底层基础代码时,动态代理的作用就非常大。

动态代理和AOP

  1. 根据前面介绍的ProxyInvocationHandler,实在很难看出这种动态代理的优势。下面介绍一种更实用的动态代理机制。

为什么使用动态代理:
开发实际应用的软件系统时,通常会存在相同代码段重复出现的情况,在这种情况下,对于许多刚开始从事软件开发的人而言,他们的做法是:选中那些代码,一路“复制”“粘贴”。但当需要修改这个相同代码段时,就要修改多份,这样显然是不科学的。

这种情况下,大部分稍有经验的开发者都会将相同代码段定义成一个方法A,然后让其用到的地方直接调用该方法A即可。但是依然会产生一个重要的问题:所有调用方法A的地方都与方法A耦合在一起了。

最理想的效果应该是:调用方法A 的地方 既可以执行方法A,又无须在程序中以硬编码方式直接调用方法A,这时就可以通过动态代理来达到这种效果。

例子
由于JDK动态代理只能为接口创建动态代理,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在该接口里定义了两个方法:

  1. public interface Dog{
  2. void info();
  3. void run();
  4. }

上面接口里只是简单地定义了两个方法,并未提供方法实现。如果直接使用Proxy为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。实际情况通常是,软件系统会为该Dog接口提供一个或多个实现类。此处先提供一个简单的实现类:GunDog。

  1. public class GunDog implements Dog{
  2. public void info(){
  3. system.out.println("我是一只猎狗”);
  4. }
  5. public void run(){
  6. system.out.println("我奔跑迅速”);
  7. }
  8. }

上面代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。再看需要实现的功能:调用方法A 的地方 既可以执行方法A,又无须在程序中以硬编码方式直接调用深色代码的方法。此处假设info()、run()两个方法代表 调用方法A 的地方,那么要求:程序执行info()、run()方法时能调用某个通用方法,但又不想以硬编码方式调用该方法。下面提供一个DogUtil类,该类里包含两个通用方法。

  1. public class DogUtil{
  2. //第一个拦截器方法
  3. public void method1(){
  4. System.out.println("==我是通用方法一==");
  5. }
  6. //第二个拦截器方法
  7. public void method2(){
  8. System.out.println("==我是通用的方法二==");
  9. }
  10. }

借助于Proxy和InvocationHandler就可以实现:当程序调用info()方法和run()方法时,系统可以“自动”将method1()和method2()两个通用方法插入info()和run()方法中执行。
这个程序的关键在于下面的MyInvokationHandler类,该类是一个InvocationHandler实现类,该实现类的invoke()方法将会作为代理对象的方法实现,如下:

  1. public class MyInvokationHandler implements InvocationHandler{
  2. private Object target; //需要被代理的对象
  3. public void setTarget(Object target){
  4. this.target = target;
  5. }
  6. //执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
  7. public Object invoke(Object proxy,Method method,Object[] args){
  8. var du = new DogUtil();
  9. du.method1(); //执行DogUtil对象中的method1方法
  10. //以target作为主调来执行method方法
  11. Object result = method.invoke(target, args);
  12. //执行DogUtil对象中的method2方法
  13. du.method2();
  14. return result;
  15. }
  16. }

上面程序实现invoke()方法时包含了一行关键代码(method.invoke),这行代码通过反射以target作为主调来执行method方法,这就是回调了target对象的原有方法。在粗体字代码之前调用DogUtil对象的method1()方法,在粗体字代码之后调用DogUtil对象的method2()方法。

下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例:

  1. public class MyProxyFactory{
  2. //为指定的target生成动态代理对象
  3. public static Object getProxy(Object target){
  4. //创建一个MyInvokationHandler对象
  5. MyInvokationHandler handler = new MyInvokationHandler();
  6. //为MyInvokationHandler设置target对象
  7. handler.setTarget(target);
  8. //创建并返回一个动态代理
  9. return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler);
  10. }
  11. }

上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法,从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler对象的invoke()方法。例如,调用动态代理对象的info()方法,程序将开始执行invoke()方法,其执行步骤如下:
① 创建DogUtil实例。
② 执行DogUtil实例的method1()方法。
③ 使用反射以target作为调用者执行info()方法。
④ 执行DogUtil实例的method2()方法。

  1. 当使用动态代理对象来代替target对象时,代理对象的方法就实现了前面的要求,程序执行info()、run()方法时既能“插入”method1()、method2()通用方法,但GunDog的方法中又没有以硬编码方式调用method1()和method2()方法。

下面提供一个主程序来测试这种动态代理的效果:

  1. public class Test {
  2. public static void main(String[] args){
  3. //创建一个原始的GunGog对象,作为target
  4. Dog target = new GunDog();
  5. //以指定的target来创建动态代理对象
  6. var dog = (Dog) MyProxyFactory.getProxy(target);
  7. dog.info();
  8. dog.run();
  9. }
  10. }
  11. 控制台
  12. ==================
  13. ==模拟第一个通用方法==
  14. 我是一只猎狗
  15. ==模拟通用方法二==
  16. ==模拟第一个通用方法==
  17. 我奔跑迅速
  18. ==模拟通用方法二==

上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行dog的info()和run()方法时,实际上会先执行DogUtil的method1()方法,再执行target对象的info()和run()方法,最后执行DogUtil的method2()方法。

  1. 采用动态代理可以非常灵活地实现解耦。通常而言,使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象生成动态代理。

这种动态代理在AOP(Aspect Orient Programming,面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理。

AOP代理的方法与目标对象的方法示意图:
反射 - 图1

反射和泛型

  • 泛型和Class类

使用Class泛型可以避免强制类型转换。例如,下面提供一个简单的对象工厂,该对象工厂可以根据指定类来提供该类的实例。

反射 - 图2

上面程序中两行粗体字代码根据指定的字符串类型创建了一个新对象,但这个对象的类型是Object,因此当需要使用CrazyitObjectFactory的getInstance()方法来创建对象时,将会看到如下代码:
反射 - 图3
甚至如下代码:
反射 - 图4

上面代码在编译时不会有任何问题,但运行时将抛出ClassCastException异常,因为程序试图将一个Date对象转换成JFrame对象。
如果将上面的CrazyitObjectFactory工厂类改写成使用泛型后的Class,就可以避免这种情况。

反射 - 图5
在上面程序的getInstance()方法中传入一个Class参数,这是一个泛型化的Class对象,调用该Class对象的newInstance()方法将返回一个T对象,如程序中粗体字代码所示。接下来当使用CrazyitObjectFactory2工厂类的getInstance()方法来产生对象时,无须使用强制类型转换,系统会执行更严格的检查,不会出现ClassCastException运行时异常。


前面介绍使用Array类来创建数组时,曾经看到如下代码:
反射 - 图6
对于上面的代码其实使用并不是非常方便,因为newInstance()方法返回的确实是一个String[]数组,而不是简单的Object对象。如果需要将arr对象当成String[]数组使用,则必须使用强制类型转换——这是不安全的操作。
Tips:
奇怪的是,Array的newInstance()方法签名为如下形式:
public static Object newInstance(Class<?> componentType, int… dimensions)

在这个方法签名中使用了Class<?>泛型,但并没有真正利用这个泛型;如果将该方法签名改为如下形式:
public static T[] newInstance(Class componentType, int length)
这样就可以在调用该方法后无须强制类型转换了。不过,这个方法暂时只能创建一维数组,也就不能利用可变个数的参数优势了。

为了示范泛型的优势,可以对Array的newInstance()方法进行包装。
反射 - 图7
上面程序中粗体字代码定义的newInstance()方法对Array类提供的newInstance()方法进行了包装,将方法签名改成了public static T[] newInstance(Class componentType, int length),这就保证程序通过该newInstance()方法创建数组时的返回值就是数组对象,而不是Object对象,从而避免了强制类型转换。
Tips:程序在①行代码处将会有一个unchecked编译警告,所以程序使用了@SuppressWarnings来抑制这个警告信息。

使用反射来获取泛型信息

  1. 通过指定类对应的Class对象,可以获得该类里包含的所有成员变量,不管该成员变量是使用private修饰,还是使用public修饰。获得了成员变量对应的Field对象后,就可以很容易地获得该成员变量的数据类型,即使用如下代码即可获得指定成员变量的类型。<br />//获取成员变量f的类型<br />Class<?> a = f.getType();<br />但这种方式只对普通类型的成员变量有效。如果该成员变量的类型是有泛型类型的类型,如Map<String,Integer>类型,则不能准确地得到该成员变量的泛型参数。

为了获得指定成员变量的泛型类型,应先使用如下方法来获取该成员变量的泛型类型:
//获取成员变量f的泛型类型
Type gType = f.getGenericType();

然后将Type对象强制类型转换为ParameterizedType对象,ParameterizedType代表被参数化的类型,也就是增加了泛型限制的类型。ParameterizedType类提供了如下两个方法:
➢ getRawType():返回没有泛型信息的原始类型。
➢ getActualTypeArguments():返回泛型参数的类型。

下面是一个获取泛型类型的完整程序:
反射 - 图8
上面程序中的粗体字代码就是取得泛型类型的关键代码。运行上面程序,将看到如下运行结果:
反射 - 图9

从上面的运行结果可以看出,使用getType()方法只能获取普通类型的成员变量的数据类型;对于增加了泛型的成员变量,应该使用getGenericType()方法来取得其类型。

Tips:
Type也是java.lang.reflect包下的一个接口,该接口代表所有类型的公共高级接口,Class是Type接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型等。