前言
突然想到可以一句话概括什么是反射:如果我们不需要import就能获取一个类,那这就是用到了反射。
什么是反射
要理解什么是反射,我们先看看什么是正射,一个常见的获取Student的正射如下:
1 | Student student = new Student(); |
通常 我们都是直接声明,或者通过new Student()
直接获取一个Student
类,然后再使用。而一个反射的例子如下:
1 | /* |
也就是说,先获取实例的Class
类,然后再通过其Class类生成一个Student
的Instance
。以上两种方式(new Student和clz.newInstance)是效果是等价的,都是获取到了一个Student
的实例。
那么我们来看看Oracle官方文档中关于反射的介绍:
1 | Uses of Reflection |
大致意思是,反射使应用程序可以通过使用它们的完全限定名创建(外部的或用户定义的)对象的实例。反射通常由需要能够检查或修改在Java虚拟机中运行的应用程序的运行时行为的程序使用。这是一个相对高级的功能,应该只由对Java语言基础有很强掌握的开发人员使用。
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的作用
一个类的成员包括以下三种:域信息、构造器信息、方法信息。而反射则可以在运行时动态获取到这些信息,在使用反射时,我们常用的类有以下五种。
反射的使用
获取Class
获取Class有如下三种方式:
1、通过全限定名获取
1 | Class clz = Class.forName("com.demo.Student") |
2、通过类获取
1 | Class clz = Student.class |
3、通过实例获取
1 | //stu是一个类的实例 |
获取Constructor
一个Student类如下:
1 | public class Student { |
当我们已经获取到了Class 对象clz,可以使用API public Constructor<T> getConstructor(Class<?>... parameterTypes)
获取这个Class 对象的构造器。文档对该API介绍如下:
返回一个Constructor对象,该对象反映了此Class对象表示的类的指定公共构造函数。parameterTypes参数是一个Class对象数组,这些对象按照声明的顺序标识构造函数的形参类型。 如果此Class对象表示在非静态上下文中声明的内部类,则形参类型包括显式封闭实例作为第一个参数。
所以我们可以通过如下方法获取Student
的构造器
1 | Constructor constructor = clz.getConstructor(); |
在我们没有显式指定构造函数的情况下,系统会自动生成一个空构造函数。所以这里的parameterTypes是空。接下来,我们为Student编写一个构造函数如下:
1 | public Student(String name, int age) { |
这时,我们再通过clz.getConstructor()
获取构造器会发生什么?
答案是能编译,但运行报错Exception in thread "main" java.lang.NoSuchMethodException: com.demo.Student.<init>()
回顾一下,之前Java类加载中提到过的<init>
是实例初始化的的一个方法。字节码文件如下:
可见,如果构造函数带参,则在<init>()
方法中会有一个MethodParameters
。如果无参,则没有这个东西。如果对象只含带参构造函数,那么就不能通过clz.getConstructor()
获取这一带参构造函数,正如我们不能通过new Student()
实例化这个对象一样。这里正确的获取构造函数的方法如下:
1 | Constructor constructor = clz.getConstructor(String.class , Integer.class); |
⚠️getConstructor()只能获取类中被声明为public的构造函数,而getDeclaredConstructor()则可以获取类中被声明的所有构造函数。
获取Field
getFields()方法
通过clz.getField()方法可以获取一个类中被声明为public(包括父类中)的成员变量。在这里我们对Student类进行一些修改,让它继承父类Person
1 | public class Person { |
然后,将Student修改如下:
1 | public class Student extends Person{ |
最后,获取Student的Field
并打印。
1 | Class clz = Class.forName("com.example.test.test.Student"); |
结果如下:
1 | public java.lang.String com.demo.Student.name |
可以发现getField只获取了被声明为public
的(包含父类中的)成员变量。另外,Field存储了其声明类型、访问控制修饰符和字段命名。如果我们只需要获取其名字,可以通过field.getName()
的方式,输出如下:
1 | name |
getField(String name)方法
通过public Field getField(String name)
方法,我们可以获取指定的成员变量,这里的参数String name
是成员变量的名字。比如我们要获取Student的sex字段,可以通过如下方式:
1 | Field field = clz.getField("sex"); |
输出结果如下:
1
public java.lang.String com.example.test.test.Person.sex
getDeclaredFields()方法
顾名思义,getDeclaredFields()方法可以获取在一个类中(不包含父类)声明的所有成员变量。我们获取并打印Student的DeclaredFields如下:
1 | Field[] fields = clz.getDeclaredFields(); |
输出结果如下:
1 | public java.lang.String com.example.test.test.Student.name |
getDeclaredField(String name)方法
正如getField()方法,通过public Field getDeclaredField(String name)
方法,我们可以获取在本类中声明的指定的成员变量。如下:
1 | Field field = clz.getDeclaredField("age"); |
输出结果如下:
1 | private int com.example.test.test.Student.age |
文档
最后,我们回过头来看一下Field的文档。
1 | A Field provides information about, and dynamic access to, a single field of a class or an interface. The reflected field may be a class (static) field or an instance field. |
意思是Field提供有关类或接口的单个字段的信息和动态访问。 反射字段可以是类(静态)字段或实例字段。
Field允许在 get 或 set 访问操作期间发生扩大转换,但如果发生缩小转换,则会引发IllegalArgumentException。
⚠️可能有人对这里的扩大转换和缩小转换有点不太理解。下面举个例子:
我们给Student声明一个新的成员变量id,初值为8;
1 | public Class Student{ |
然后通过getField()方法获取这一字段,
1 | Field id = clz.getField("id"); |
接着,我们实例化一个Student对象。
1 | Object student = clz.newInstance(); |
然后,获取student的id变量的值,用int正常接收;
1 | /* |
结果如下:
1 | 8 |
接着,我们看下什么是扩大转换。我们用一个Double类型的对象来接收这一int:
1 | Double id_v = id.getDouble(student); |
结果是:
1 | 8.0 |
最后,我们看下什么是缩小转换。我们用一个Short类型的对象来接收这一int:
1 | short id_v = id.getShort(student); |
结果是:
1 | Exception in thread "main" java.lang.IllegalArgumentException: Attempt to get int field "com.example.test.test.Student.age" with illegal data type conversion to short |
也就是说无法将int转化为short。
总结如下:
扩大转换就是将short转换为int、int转换为long、char转换为String等。缩小类型转换就是将int转换为short,long转换为int等
获取Method
getMethods()、getDeclaredMethods()
getMethods()、getDeclaredMethods()方法和getFields()、getDeclaredFields()一样,就不叙述过多,这里主要讲一下特别的点,
由于Java中一切皆Object的思想,所以所有的Class除了自己声明的方法,还有继承自Object的如下方法:
1 | 1、public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException |
getMethod(String name, Class>... parameterTypes)、getDeclaredMethod(String name, Class>… parameterTypes)
和getField(String name)不同的是,它多了一个参数数组parameterTypes,它们是该Method的参数数组。
除此之外,就没啥好说的了。
实例调用方法(invoke函数介绍)
需要注意的是,正如Field,我们通过反射获取到的Method也只是一个Method,它只是一个method的影子、模版。如果我们要调用这个方法,需要指定是哪个类调用了这个方法。如下:
1 | // 获取Class对象 |
运行结果如下:
1 | 18 |
获取实例(Object)
通过Class直接生成
Class实例拥有一个newInstance()方法,该方法可以快速生成一个实例。如下:
1 | Object student = clz.newInstance(); |
可是当我们运行时,却得到如下报错:
1 | Exception in thread "main" java.lang.InstantiationException: com.example.test.test.Student |
我们查看newInstance()的文档如下:
1 | Creates a new instance of the class represented by this Class object. The class is instantiated as if by a new expression with an empty argument list. The class is initialized if it has not already been initialized. |
创建由此Class对象表示的类的新实例。 类被实例化,就好像由一个带有空参数列表的new表达式一样。回想一下,我们的Student类好像只有一个带参构造函数,没有空构造函数,所以才会导致报错。在给Student添加一个无参构造函数后,不再报错,顺利获得一个Student实例。
⚠️newInstance()被标记 @Deprecated(since=”9”)。意思是这个方法在jdk9以后被弃用了。可以替换为clz.getDeclaredConstructor().newInstance()。
这个替代方案的实质还是使用Constructor生成实例,所以调用一个带参构造函数Student(String name, int age)生成实例如下:
1 | Object student = clz.getDeclaredConstructor(String.class,int.class).newInstance("ceaser",11); |
通过Constructor生成
通过上面的介绍知道,jdk将clz的newInstance废弃的替代方案为Constructor。
1 | Object student = clz.getDeclaredConstructor(String.class,int.class).newInstance("ceaser",11); |
其实等价于
1 | Constructor constructor = clz.getDeclaredConstructor(String.class,int.class); |