“造轮子”-反射机制的三大基本操作

我正在参加「掘金·启航计划」

反射概述

反射就是在运行时期,动态的获取类中成员信息(构造器,字段,方法)的过程! 反射存在的作用,在不知道对象的真实类型的情况下去调用对象真实存在的方法,所以再回过来看上面我们抛出的问题,那么使用反射技术就能解决了。

1、字节码对象

在 Java 中,万物皆对象.我们可以通过多个事物,发现他们的共性,来抽象成一个类,类就是对象的模板,而一个个的个体,就是对象. 比如人类和学生. Class.jpg 当对象多了以后,我们使用类来进行描述所有对象的特征。

那么类多了以后呢 ?

类和类之间也有共性(比如每个类都构造器,每个类都用方法,每个类都有字段),我们java中用Class来描述所有的类的共同特征。

用Class 类 来描述所有的类的特征,所以我们成Class 为类的类型

通过Class 这个类,创建出的对象,成为字节码对象

通过Class来描述所有类的共性的信息,把这些共性的信息以面向对象的思想使用对象进行了封装。所以在Class类中把类中的成员分成了三大类对象来进行管理。分别为构造器对象,方法对象,字段对象。

具体类中的成员信息和对象是怎么样一个对应关系呢?

类中的每一个方法 /每一个字段 都被封装了一个对应的对象。

JDK 中定义好的 Class 类: java.lang.Class

image-20220605192401149.png

该类中有大量的 get 开头的方法.表示可以使用字节码对象来获取信息.所以当我们拿到了字节码对象,就可以直接操作当前字节码中的构造器,方法,字段.

2、 获取字节码对象的三种方式

通过 查看API ,我们得知 Class ,没有公共的构造器,其原因是 Class 对象是在加载类时由 Java 虚拟机自动构造的。

Snip20210815_4.png

该字节码对象不是有我们去创建的,而是自动创建的 。 继续查看API发现获取字节码对象有三种方式:

Snip20210815_3.png

获取字节码的方式:

  1. 通过 Class 类的 forName() 方法来获取字节码对象.

    • Class.forName(String classsName) : 通过类的全限定名获取字节码对象

      全限定名: 包名.类型  例如:Class.forName("java.lang.String"); 
      复制代码
  2. 通过对象的 getClass() 方法来获取字节码对象

    • 对象.getClass();

      User u = new User();
      u.getClass();   // 这个 getClass() 方法,是来源于父类 Object 中的
      复制代码
  3. 通过类型(基本类型)的 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大量的在框架中使用.

代码实现:

image-20200426010732744.png

思考:

  1. 三种方式获取到的字节码是同一个吗?

    字节码只会加载一次,所有不管用的哪种方式去获取字节码,都是同一个

  2. 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);
    }
​
复制代码

上述的代码报错了,错误如下,错误信息为非法访问。 image-20200426081810803.png

问题:不能直接访问没有权限(非public)的成员

**解决方案: **反射中给出一个可以访问的方案,想要使用反射去操作非public的成员.必须设置一个可以访问的标记.

public void setAccessible(boolean flag): 传递一个true,表示可以访问,表示不管权限.
复制代码

这个方法的出现是在AccessibleObject 类中,那么API为何这样设计呢?

image-20200426081857403.png

从 API 中我们可以发现,Constructor,Field,MethodAccessibleObject 的子类,

因为这三种成员都可能有被访问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方法,来创建真实对象的。

我们把这种方式称为----创建对象的快捷方式

创建对象的快捷方式必须满足一个条件: 类中必须提供一个公共的无参数的构造器

代码如下图:

image-20200426082319103.png

经验 :

只要看到传入全限定名,基本上都是要使用反射,通过全限定名来获取字节码对象.

只要看到无指定构造器但是能创建对象,基本上都是要通过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,"小狼");
}
复制代码

注意:

  1. 方法也是可以被访问私有修饰符修饰的,所以,如果要访问非 public 修饰的方法,需要在访问之前设置可访问 method.setAccessible(true);
  2. 如果调用的是静态方法,是不需要对象的,所以此时在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);
}
复制代码

猜你喜欢

转载自juejin.im/post/7105725723406663694