反射机制

1,203 阅读12分钟

满满干货,耐心看完,一定会有收获的。

一、什么是反射

百度百科:Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种夫『动态获取程序信息以及动态调用对象』的功能称为Java语言的反射机制。反射被视为动态语言的关键。

为什么要学习反射

很多同学来学反射的原因是在学习框架的时候看到了这么一句话:反射机制是框架的基础。没错。

反射:框架设计的灵魂 框架:半成品软件。可以在框架的基础上进行软件开发,简化编码。 将类的各个组成部分封装为其他对象,然后调用其中额的方法,这就是反射机制。

停停停,我知道你看不懂,没事,看到第三篇幅之后再来品一品这句话就懂了。

反射的基本原理

先来看看一个我们写的Person类在虚拟机中的三个阶段。

Class类对象的含义是,在jdk中确实有一个名字是Class的类,它的对象就叫Class类对象(这一点很简单,比如Person类的对象就叫Person类对象嘛),只不过这里是不同的类生成不同的Class类对象而已。

一个java文件通过jdk中的编译器编译成class字节码文件后,这些文件只有被加载到JVM中才能运行和使用。现在JVM从磁盘中或者其他地方读到了Person.class了,这时候JVM就会解析Person.class产生一个对应的类对象(一个类只对应一个这种特殊的对象),这个类对象中有Person类的所有信息。现在我们想新建一个这样的实例,都是从这个特殊对象(类对象)中提取信息来组成实例,并且以后进行new Person()且无论new多少次都是用这一个唯一的对象。

反射的感觉马上就要呼之欲出了:现在我要获得Person类的信息,我不能直接从Person.class中拿,而是新建一个Person的类对象,反向获取Person类中的信息。这就是反射,我想我讲的应该还可以,很多博客和视频上要么是不够深入,要么是晦涩难懂,没有讲到要害。

反射的好处

1.在程序的运行过程中,操作那些“其他对象” 2.可以解耦,来提高的可扩展性。

乍一看还不懂,不急,我们下面慢慢来看嘛。

二、获取某个类的Class类对象

前面我们说了,class字节码被JVM拿过来分析了才得到了类对象,这个过程是JVM完成的,可是我们现在想把这个类对象拿过来看看到底有啥用,咋整?

获取Class对象的三种方式:

  • Class.forName("全类名"); 这是个静态方法,将字节码文件加载到内存(JVM),返回Class对象。注意,这里是全类名,package要写全。
  • 类名.class 当已经存在了某个类的类对象时,直接通过类名来获取它的类对象
  • 对象.getClass() 当都已经生成了某个类的一个对象了,那就可以通过这个对象来返回类对象,getClass()方法是封装在Object类中的,自然可以被任何对象调用。

说白了,这就是上文中面对的三个不同阶段来获取Class对象的合适的方法。 下面就来演示一下这三种方式:

演示代码

实体类Person:

package fanshe.pojo;

public class Person {
    private String name;
    private int age;

    public int a;
    protected int b;
    private int c;
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public  void eat(){
        System.out.println("我是eat方法!");
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", a=" + a +
                ", b=" + b +
                ", c=" + c +
                '}';
    }
}

测试类代码

package fanshe.reflect;

import fanshe.pojo.Person;

public class reflectTest {
    public static void main(String[] args) throws ClassNotFoundException {
        /**
         * 获取Class对象的三种方式方式:
         * 1.Class.forName("全类名");  将字节码文件加载到内存(JVM),返回Class对象。
         * 2.类名.class  当已经存在了某个类的类对象时,直接通过类名来获取它的类对象
         * 3.对象.getClass() 当都已经生成了某个类的一个对象了,那就可以通过这个对象来返回类对象,getClass()方法是封装在Object类中的,自然可以被任何对象调用。
         * */

        //1.Class.forName("全类名")
        Class cls1 = Class.forName("fanshe.pojo.Person");
        System.out.println("第一种方式:"+cls1);

        //2.类名.class
        Class cls2 = Person.class;
        System.out.println("第二种方式:"+cls2);

        //3.对象.getClass()
        Person p =new Person();
        Class cls3 = p.getClass();
        System.out.println("第三种方式:"+cls3);
        //比较这三个对象是不是相等的,内存地址相不相等
        System.out.println(cls1==cls2);
        System.out.println(cls1==cls3);
        //是相等的,这也验证了之前我们说的,一个类只产生这么一个类对象
    }
}

结果

Image

三种方式的使用场景

现在新问题来了,什么时候该使用什么方式呢?

三种方式分别对应的使用场景: 1.多用于配置文件,将类名定义在配置文件中。读取配置文件,加载类。 2.多用于参数的传递。 3.多用于对象来获取字节码。

三、Class对象到底有啥用

那么Class对象到底有啥用呢?我们通过可以查看JDK API(中的Class类)来看Class下实现的方法,下面是博主总结的几个功能和对应方法。

获取类中信息,封装成新的对象的功能

将类中的信息包装成新对象返回得到,边看下文的演示代码边来读下面这些方法

①获取Filed(成员变量)对象

  • Field getField(String name)
    返回一个 Field对象,它反映此表示的类或接口的指定公共成员字段。
  • Field[] getFields() 返回包含一个数组 Field对象反射由此表示的类或接口的所有可访问的公共字段(public)。
  • Field getDeclaredField(String name) 返回一个 Field对象,它反映此表示的类或接口的指定所有已声明字段。
  • Field[] getDeclaredFields() 返回的数组 Field对象反映此表示的类或接口声明的所有字段。

等等,你是说获取所有?private?私有的都能获取?

是的,答案是能,在反射面前,都是透明的。但是!!!也不能那么简简单单就看见啊,直接和public那样的去访问的时候会报一个IllegalAccess错误,需要加上一个忽略代码:setAccessible(true)。 具体的见下文中的Field对象的操作功能和演示代码。

②获取Constructor(构造方法)对象

  • Constructor getConstructor(类<?>... parameterTypes)返回一个 Constructor对象,该对象反映 Constructor对象表示的类的指定的公共构造函数。
  • Constructor<?>[] getConstructors() 返回包含一个数组 Constructor对象反射由此表示的类的所有公共构造。
  • Constructor getDeclaredConstructor(类<?>... parameterTypes) 返回一个 Constructor对象,该对象反映 Constructor对象表示的类或接口的指定类函数。
  • Constructor<?>[] getDeclaredConstructors() 返回一个反映 Constructor对象表示的类声明的所有 Constructor对象的数组类 。

③获取Method对象(方法)

  • Method getMethod(String name, 类<?>... parameterTypes)返回一个 方法对象,它反映此表示的类或接口的指定公共成员方法。
  • Method[] getMethods()返回包含一个数组 方法对象反射由此表示的类或接口的所有公共方法 类对象,包括那些由类或接口和那些从超类和超接口继承的声明。
  • Method getDeclaredMethod(String name, 类<?>... parameterTypes) 返回一个 方法对象,它反映此表示的类或接口的指定声明的方法。
  • Method[] getDeclaredMethods() 返回包含一个数组 方法对象反射的类或接口的所有声明的方法,通过此表示 类对象,包括公共,保护,默认(包)访问和私有方法,但不包括继承的方法。

思考:为啥参数里面有了方法的name(用于确定哪个方法)后面还要跟其他参数呢?

因为方法可以重载啊!!! 同名不同参!!! 豁然开朗!!! 其实在看Construct的获得的时候就应该意识到这个问题了。

④获取类名

String getName() 返回由 类对象表示的实体(类,接口,数组类,原始类型或空白)的名称,作为 String 。

返回的这些对象有啥用?

获取了一些类中的信息并包装成对象类型,我们希望去获得这些信息的值甚至改变他们,各种操作功能呼之欲出。 同样我们可以查看API来看他们都有什么方法。

Field对象:成员变量

查看Field类的API:

1.设置值 set 2.获取值 get 3.忽略权限问题的暴力反射(见演示代码) 需要传递真实的对象。

Constructor对象:构造方法

查看Construct类的API:

1.创建对象: newInstance(),根据获取到的构造方法对象,看看需不需要传入参数即可(看演示代码)。 注:对于无参构造,Class类中直接给了一个方法,调用类对象中的newInstance就行了,不用先获取Construct对象,再调用这么麻烦了。 2.和前面一样的暴力反射

Method对象:普通方法

查看Method 类的API

1.调用方法(执行方法) 需要传递真实的对象 2.获取方法名称 String getName() 较为简单不解释了

到这里基本的一些方法已经讲完了,怕大家还是有些糊涂,特地去画了张图来表示上面这些方法的关系。

对象及其方法图

Image

package fanshe.reflect;

import fanshe.pojo.Person;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectFunction {
    public static void main(String[] args) throws Exception {
        //先获取Person的类对象
        Class cls = Class.forName("fanshe.pojo.Person");

        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        //演示:Filed类型

        //获取所有public修饰的成员变量
        Field[] fields = cls.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        //获取public成员变量a
        Field field_a =cls.getField("a");//获取类中定义的a字段
        //这时候去实例化一个Person对象,我们希望通过上面的Filed a 来查看、设置具体对象里面的a变量
        //还记得么,这就是反射啊,我不直接看person对象内容,而是通过Person类的类对象来查看你的内容。是不是印象又加深了。
        Person person = new Person();
        Object o = field_a.get(person);
        System.out.println(o);//打印出0,因为类中定义的a 是int型,且未赋值
        field_a.set(person,5);//设置person中a变量的值为5
        System.out.println(field_a.get(person));

        //获取private成员变量c
        Field field_c=cls.getDeclaredField("c");
        //忽略访问权限修饰的安全检查_暴力反射
        field_c.setAccessible(true);
        System.out.println(field_c.get(person));//成功访问,打印为0

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        //演示:构造方法
        Constructor constructor= cls.getConstructor(String.class,int.class);//获取Person类中那个含餐的构造函数
        //System.out.println(constructor);
        Object newPerson = constructor.newInstance("哈哈哈", 4);
        System.out.println(newPerson.toString());//成功打印出 哈哈哈,4 这俩个字段

        //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        //演示:普通方法
        Method method_eat = cls.getMethod("eat");
        Person p=new Person();
        //执行方法
        method_eat.invoke(p);//成功调用eat方法
        //注:上述代码只是部分展示,我想大家应该会举一反三来实现不同修饰下的不同对象和他们可以用的各种方法。
    }
}

现在你回头看第一个标题下,“将类的各个组成部分封装为其他对象”这句话是不是就豁然开朗了。

四、写一个框架

你还不太理解这个反射?没事,我们下面从稍微宏观的角度上再来理解一下。给一个案例! 需求:写一个“框架”,可以帮我们创建任意类的对象,并且执行其中的任意方法。 代码结构:Animal.java实体类,reflect.properties资源配置文件,Frame.java充当框架 直接看代码,里面的注解很详细了。

Animal.java实体类

package fanshe.pojo;

public class Animal {
    public void sleep(){
        System.out.println("执行Animal下的sleep方法:我是动物,我爱睡觉!");
    }
}

reflect.properties资源配置文件

className=fanshe.pojo.Animal
methodName=sleep

Frame.java框架类

package fanshe.reflect;
import java.io.InputStream;
import java.util.Properties;

/**
 * 我是一个框架
 * 不能改变我的任何代码
 * 能通过我创建任意类的对象并调用任意方法
 * 用配置文件和反射来实现这个需求
 *
 * 将需要创建的对象的全类名和需要的方法定义在配置文件中
 * 在程序中加载读取配置文件
 * 使用反射技术来加载类文件(字节码)进内存,生成对应Class类对象
 * 生成实例对象,执行一些方法
 * */
public class Frame {
    public static void main(String[] args) throws Exception {
        //创建好配置文件后,创建Properties对象来加载这个配置文件
        Properties reflectPro =new Properties();
        //加载并转换为一个Map集合
        //获取当前class目录下的对应的配置文件
        ClassLoader classLoader = Frame.class.getClassLoader();//这个方法在Class下,是返回类加载器的
        InputStream resourceAsStream = classLoader.getResourceAsStream("reflect.properties");//获得资源文件的字节流
        reflectPro.load(resourceAsStream);

        //+++++++++++++

        //加载好资源文件后,获取配置文件中定义的数据
        String className = reflectPro.getProperty("className");//这就是待会在反射中要用的全类名
        String methodName = reflectPro.getProperty("methodName");

        //加载该类,获得类对象
        Class cls = Class.forName(className);
        //创建对象
        Object o = cls.newInstance();
        cls.getMethod(methodName).invoke(o);

        //所以以后我再想执行什么类的什么方法,我就直接去改资源文件就行。
    }
}

注:资源文件放在src下一级目录中,且注意编码方式要和其他类文件一样(UTF-8准没错),文件内容不要用中文。

结果

Image

五、总结

现在让我们再回过头来,一开始我们去学习Maven,mybatis,spring框架的时候,是不是都是有资源文件,然后有工具类或者其他类来读取资源配置文件,这样我们就可以不用去花心思考虑一些数据库链接,其他配置问题,把主要注意力都放在了具体的逻辑代码的编写上。

我们从一开始学习框架了解到反射机制,然后慢慢揭开了它神秘的面纱,甚至现在我们已经能够用反射机制搭一个简单的框架了。 如果还不明白反射咋回事,没关系,回头再看一遍,或者底下留言,我们共同交流学习进步!

注:需要JDK API的给我留言即可,人手一份中文文档对于Java学习还是很有用的。