我正在参加「掘金·启航计划」
反射概述
反射就是在运行时期,动态的获取类中成员信息(构造器,字段,方法)的过程! 反射存在的作用,在不知道对象的真实类型的情况下去调用对象真实存在的方法,所以再回过来看上面我们抛出的问题,那么使用反射技术就能解决了。
1、字节码对象
在 Java 中,万物皆对象.我们可以通过多个事物,发现他们的共性,来抽象成一个类,类就是对象的模板,而一个个的个体,就是对象. 比如人类和学生. 当对象多了以后,我们使用类来进行描述所有对象的特征。
那么类多了以后呢 ?
类和类之间也有共性(比如每个类都构造器,每个类都用方法,每个类都有字段),我们java中用Class来描述所有的类的共同特征。
用Class 类 来描述所有的类的特征,所以我们成Class 为类的类型
通过Class 这个类,创建出的对象,成为字节码对象
通过Class来描述所有类的共性的信息,把这些共性的信息以面向对象的思想使用对象进行了封装。所以在Class类中把类中的成员分成了三大类对象来进行管理。分别为构造器对象,方法对象,字段对象。
具体类中的成员信息和对象是怎么样一个对应关系呢?
类中的每一个方法 /每一个字段 都被封装了一个对应的对象。
JDK 中定义好的 Class 类: java.lang.Class
该类中有大量的 get 开头的方法.表示可以使用字节码对象来获取信息.所以当我们拿到了字节码对象,就可以直接操作当前字节码中的构造器,方法,字段.
2、 获取字节码对象的三种方式
通过 查看API ,我们得知 Class ,没有公共的构造器,其原因是 Class 对象是在加载类时由 Java 虚拟机自动构造的。
该字节码对象不是有我们去创建的,而是自动创建的 。 继续查看API发现获取字节码对象有三种方式:
获取字节码的方式:
-
通过 Class 类的 forName() 方法来获取字节码对象.
-
Class.forName(String classsName) : 通过类的全限定名获取字节码对象
全限定名: 包名.类型 例如:Class.forName("java.lang.String"); 复制代码
-
-
通过对象的 getClass() 方法来获取字节码对象
-
对象.getClass();
User u = new User(); u.getClass(); // 这个 getClass() 方法,是来源于父类 Object 中的 复制代码
-
-
通过类型(基本类型)的 class 字段来获取字节码对象
-
int.class
为何基本数据类型可以通过.class属性呢? 在jdk文档中是这样描述的 原文: The primitive Java types (boolean, byte, char, short, int, long, float, and double), and the keyword void are also represented as Class objects. 理解: 基本Java类型(boolean、byte、char、short、int、long、float和double)以及关键字void也表示为类对象。 复制代码
-
三种方式的区别:
在编译时期:
最后两种你必须明确具体的类的类型,否则编译不通过 第一种后面是指定这种类型的字符串形式就行,编译没问题。(推荐)
后面学习框架,配置文件中都是使用字符串,也就是类的全限定名(全类路径/包类路径),这样解析读取配置文件中类的全限定名就可以通过反射创建对象了,通过 Class.forName大量的在框架中使用.
代码实现:
思考:
-
三种方式获取到的字节码是同一个吗?
字节码只会加载一次,所有不管用的哪种方式去获取字节码,都是同一个
-
int 类型和 int[] 它们的字节码是同一个吗?
int 类型和int数据类型不是同一个
3、 通过反射,创建类的真实实例对象
我们创建一个实例对象,是通过调用构造方法来完成的。如:
Person p = new Person() ; 这Person对象,就是通过调用无参数的构造器完成的。
复制代码
步骤:
1、 获取构造方法对应的 构造器对象
2、 通过该构造器对象,调用构造方法,创建真实对象
获取构造器对象
通过查看API,发现获取构造器对象的方法有四个:
-
获取所有的构造器对象
public Constructor<?>[] getConstructors()
: 获取所有的 public 修饰的构造器public Constructor<?>[] getDeclaredConstructors()
: 获取所有的构造器(包括非public) -
获取指定的构造器对象
public Constructor<T> getConstructor(Class... parameterTypes)
public Constructor<T> getDeclaredConstructor(Class... parameterTypes)
: parameterTypes : 参数的类型(构造方法的参数列表的类型).注意: 找构造器/方法,传递的是参数的类型.
结论 : 带着 s 表示获取多个.带着 Declared 表示忽略权限,包括私有的也可以获取到.
准备一个Person类,通过反射来操作Person类中的构造器
public class Person {
public Person(){
System.out.println("这是公共的构造器");
}
public Person(int age){
System.out.println("这是公共的构造器并带一个int类型的参数"+ age);
}
private Person(String name){
System.out.println("这是私有的构造器并带一个字符串类型的参数" + name);
}
private Person(String name,Long age){
System.out.println("这是私有的构造器并带两个参数,一个String类型,一个Long类型" + name + age);
}
}
复制代码
代码演示:
@Test
public void testGetAllConstructors() throws NoSuchMethodException {
// 获取字节码对象
Class clz = Person.class;
//获取所有 public 构造器
Constructor[] cons1 = clz.getConstructors();
for(Constructor con : cons1){
System.out.println(con);
}
//获取所有构造器,包括 private
Constructor[] cons2 = clz.getDeclaredConstructors();
for(Constructor con : cons2){
System.out.println(con);
}
//获取公共的无参构造器
Constructor con1 = clz.getConstructor();
System.out.println(con1);
//获取公共的带一个参数的构造器
Constructor con2 = clz.getConstructor(int.class);
System.out.println(con2);
//获取指定 private并且带两个参数的 构造器
Constructor con3 = clz.getDeclaredConstructor(String.class, Long.class);
System.out.println(con3);
}
复制代码
调用构造器方法,创建真实对象
JDK给我们提供一个newInstance的方法,用来创建真实对象
public Object newInstance(Object... initargs)
// initargs: 调用该构造器传递的实际参数.参数列表一定要匹配(类型,个数,顺序).
复制代码
通过代码演示真实对象的创建
代码演示:
//通过调用公共的带一个参数的构造方法,来创建对象
@Test
public void testCreateObject() throws Exception {
// 获取字节码对象
Class clz = Class.forName("cn.wolfcode._04_reflect.Person");
// 获取公共的带一个参数的构造器,参数为参数类型
Constructor con = clz.getConstructor(int.class);
//调用构造器
Object obj = con.newInstance(24);
System.out.println(obj);
}
复制代码
//通过调用私有的带两个参数的构造方法,来创建真实类的对象
@Test
public void testCreateObject2() throws Exception {
// 获取带有参数的 private 构造器
Constructor con2 = clz.getDeclaredConstructor(String.class,Long.class);
Object obj2 = con2.newInstance("小狼",12L);
System.out.println(obj2);
}
复制代码
上述的代码报错了,错误如下,错误信息为非法访问。
问题:不能直接访问没有权限(非public)的成员
**解决方案: **反射中给出一个可以访问的方案,想要使用反射去操作非public的成员.必须设置一个可以访问的标记.
public void setAccessible(boolean flag): 传递一个true,表示可以访问,表示不管权限.
复制代码
这个方法的出现是在AccessibleObject 类中,那么API为何这样设计呢?
从 API 中我们可以发现,Constructor,Field,Method
是AccessibleObject
的子类,
因为这三种成员都可能有被访问private 修饰符修饰的.因此每一个类(Constructor,Field,Method)中要提供setAccessible方法,那放到父类中更为妥当。
所以可以改为如下这种方式获取:
//通过调用私有的带两个参数的构造方法,来创建真实类的对象
@Test
public void testCreateObject() throws Exception {
// 获取带有参数的 private 构造器
Constructor con2 = clz.getDeclaredConstructor(String.class,Long.class);
// 调用私有构造器,必须先设置为可访问
con2.setAccessible(true);
// 创建真实对象
Object obj2 = con2.newInstance("小狼",12L);
System.out.println(obj2);
}
复制代码
在Class类,同样也提供一个newInstance方法,来创建真实对象的。
我们把这种方式称为----创建对象的快捷方式
创建对象的快捷方式必须满足一个条件: 类中必须提供一个公共的无参数的构造器
代码如下图:
经验 :
只要看到传入全限定名,基本上都是要使用反射,通过全限定名来获取字节码对象.
只要看到无指定构造器但是能创建对象,基本上都是要通过Class对象的 newInstance 去创建对象.
4、 通过反射,调用对象中的真实方法
通过反射来调用对象中的方法,必须先要获取到方法对应的方法对象。所以要把上面的目标进行分解,分解为两个小目标:
步骤:
1、 获取方法对应的 方法对象
2、通过方法对象,调用方法
反射获取方法对象
通过查看API,发现获取方法对象的方法有四个:
获取所有方法:
public Method[] getMethods()
: 可以获取到所有的公共的方法,包括继承的.+public Method[] getDeclaredMethods()
:获取到本类中所有的方法,包括非public的,不包括继承的.
获取指定的方法:
public Method getMethod(String name, Class<?>... parameterTypes)
:public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
: name: 方法名 parameterTypes: 当前方法的参数列表的类型.- 注意,要找到某一个指定的方法,必须要使用方法签名才能定位到.而方法签名=方法名+参数列表,还记得我们获取构造器的经验吗?带着s表示获取多个,带着declared表示忽略访问权限.
准备一个Person类,通过反射来操作Person类中的方法
public class Person {
public void sayHello(String name){
System.out.println("公共的普通的方法,带一个String类型的参数");
}
private void doWork(String name){
System.out.println("私有的普通方法,带一个String类型的参数");
}
public static void sayHello(String name,Long id){
System.out.println("调用静态方法");
}
private void doWork(){
System.out.println("doWork");
}
}
复制代码
代码演示
@Test
public void testGetAllMethod() throws Exception {
// 获取字节码对象
Class clz = Class.forName("cn.wolfcode._01_reflect.Person");
//获取所有 public 方法,包括父类的
Method[] methods = clz.getMethods();
for(Method m : methods){
System.out.println(m);
}
System.out.println("------------------");
/获取所有方法,包括 private 不包括父类的
Method[] methods2 = clz.getDeclaredMethods();
for(Method m : methods2){
System.out.println(m);
}
System.out.println("------------------");
//获取指定参数的 public 的方法
Method sayHelloMethod = clz.getMethod("sayHello", String.class);
System.out.println(sayHelloMethod);
System.out.println("------------------");
//获取指定参数的private 方法
Method doWorkMethod = clz.getDeclaredMethod("doWork", String.class);
System.out.println(doWorkMethod);
}
复制代码
调用方法
通过查看 Method这个类的 API发现 : 给我们提供了一个 invoke 方法 ,来完成真实方法的
public Object invoke(Object obj, Object... args):
复制代码
obj: 表示调用该方法要作用到那个对象上. args:调用方法的实际参数
方法的返回值表示,调用该方法是否有返回值,如果有就返回,如果没有,返回null.
通过代码演示方法的被调用
代码实现:
//调用sayHello带一个参数的方法
@Test
public void testGetMethod() throws Exception {
// 步骤1. 获取字节码对象
Class clz = Class.forName("cn.liu.reflect.Person");
// 步骤2. 创建真实实例对象
Object obj = clz.newInstance(); // 使用公共的无参数的构造器
// 步骤3. 获取sayHello方法并且不带参数的方法对象
Method sayHelloM = clz.getMethod("sayHello",String.class);
// 步骤4. 调用方法
sayHelloMethod.invoke(obj, "小liu");
}
复制代码
//调用sayHello带两个参数的方法
@Test
public void testGetMethod() throws Exception {
// 步骤1. 获取字节码对象
Class clz = Class.forName("cn.liu.reflect.Person");
// 步骤2. 创建真实实例对象
Object obj = clz.newInstance(); // 使用公共的无参数的构造器
// 步骤3. 获取sayHello方法并且不带参数的方法对象
Method sayHelloM = clz.getMethod("sayHello",String.class,Long.class);
// 步骤4. 调用方法
sayHelloMethod.invoke(obj, "小狼",10L);
}
// 步骤4. 调用方法
sayHelloMethod.invoke(null, "小狼",10L);
复制代码
注意:这个带两个参数的sayHello的方法,是静态的,那么它可以有类直接去调用 因此 ,在调用invoke方法的时候,可以把obj,换成null
//调用doWork不带参数的方法
@Test
public void testGetMethod() throws Exception {
// 步骤1. 获取字节码对象
Class clz = Class.forName("cn.wolfcode._01_reflect.Person");
// 步骤2. 创建真实实例对象
Object obj = clz.newInstance(); // 使用公共的无参数的构造器
// 步骤3. 获取sayHello方法并且不带参数的方法对象
Method sayHelloM = clz.getMethod("doWork");
// 步骤4. 设置可访问
doWorkMethod.setAccessible(true);
// 步骤5. 调用方法
sayHelloMethod.invoke(obj);
}
复制代码
//调用doWork带一个String类型的参数的类修
@Test
public void testGetMethod() throws Exception {
// 步骤1. 获取字节码对象
Class clz = Class.forName("cn.wolfcode._01_reflect.Person");
// 步骤2. 创建真实实例对象
Object obj = clz.newInstance(); // 使用公共的无参数的构造器
// 步骤3. 获取sayHello方法并且不带参数的方法对象
Method sayHelloM = clz.getMethod("doWork",String.class);
// 步骤4. 设置可访问
doWorkMethod.setAccessible(true);
// 步骤5. 调用方法
sayHelloMethod.invoke(obj,"小狼");
}
复制代码
注意:
- 方法也是可以被访问私有修饰符修饰的,所以,如果要访问非 public 修饰的方法,需要在访问之前设置可访问 method.setAccessible(true);
- 如果调用的是静态方法,是不需要对象的,所以此时在invoke方法的第一个参数,对象直接传递一个null 即可.
5、通过反射,操作对象中的属性
通过反射来调用对象中的字段,必须先要获取到字段对应的字段对象。所以要把上面的目标进行分解,分解为两个小目标:
步骤:
1、 获取字段对应的字段对象
2、 通过字段对象,修改字段内容
准备一个Person类,通过反射来操作Person类中的字段
public class Person {
private String name;
public Long id;
public Integer age;
}
复制代码
获取字段对象
通过查看字段(Field)的API发现,操作字段信息的方法有四个:
获取所有字段
public Field[] getFields()
public Field[] getDeclaredFields()
获取单个字段:
public Field getField(String name)
: name 要获取的字段的名称 public Field getDeclaredField(String name)
:
通过代码演示字段对象的获取
@Test
public void testField() throws Exception {
// 获取字节码对象
Class clz = Person.class;
//获取所有公共的字段信息
Field[] fs = clz.getFields();
for(Field f: fs){
System.out.println(f);
}
System.out.println("---------------------");
//获取所有字段信息包括私有
Field[] fs2 = clz.getDeclaredFields();
for(Field f: fs2){
System.out.println(f);
}
System.out.println("----------------------");
//获取自定名称的字段对象:name
Field nameField = clz.getDeclaredField("name");
System.out.println(nameField);
}
复制代码
操作字段
通过查看文档API,发现操作字段的就是set开头 和 get开头方法
get(Object obj);
set(Object obj,Object value)
复制代码
通过代码演示操作字段的内容
@Test
public void testField() throws Exception {
// 获取字节码对象
Class clz = Person.class;
// 注意:如果给字段设置内容,
// 必须保证设置字段传入的对象和获取字段传入的对象是同一个
Object obj = clz.newInstance();
// 获取单个字段
Field nameField = clz.getDeclaredField("name");
System.out.println(nameField);
// 设置私有字段可访问
nameField.setAccessible(true);
// 操作name字段
// 设置那么字段的数据
nameField.set(obj, "小狼");
// 获取name字段的数据
Object nameValue = nameField.get(obj);
System.out.println(nameValue);
}
复制代码