庆哥聊Java反射机制

共 16062字,需浏览 33分钟

 ·

2021-04-23 18:30

作者 | ithuangqing

来源 | 编码之外(ID:ithuangqing)

关于Java反射那些事……

今天咱们一起聊聊Java中的反射,那些你知道的和不知道的……

有人说反射机制是比较简单的,你觉得呢?先不说简单不简单的,我只告诉你,反射不会,对你后面学习框架源码会有很大影响,但是在以后的工作中可能需要你动手去写反射的情况也很少,也就是说,如果你说你以后不准备深入研究一些框架的源码什么的,那我觉得反射你完全不用学!

什么是反射

那什么是反射呢?希望你能记住这句话:

Java反射是与Java字节码相关的,也就是javac编译之后的那个class文件

我们使用反射是可以操作这个class字节码文件的,具体的操作就包括基本的读和写了,咋一看,不明所以然,觉得有点深奥,说简单点,就是我们可以通过一定的手段去获取一个类的Class对象,也就是这个类的字节码文件,然后使用Class对象自带的一些API去执行一些操作,比如获取这个对象的一些方法和属性。

这里,我觉得首先需要理解两个概念类和对象不知道你们理解的如何,就是啥是类?啥是对象呢?

类和对象

什么是类?写一段代码:

public class Person {
    //属性
    /**
     * 年龄
     */

    int age;
    /**
     * 姓名
     */

    String name;

    //方法

    /**
     * eat
     */

    public void eating(String what){
        System.out.println(age+"岁的"+name+"正在吃"+what);
    }
}

在java中,一个类通常包含基本的属性和方法,所谓的“类”其实也好理解,我们完全可以将其类比为现实世界中的某一类东西,比如香蕉橘子和苹果都属于水果这个大类,我们这里拿人类来举例子,不管你是小明小红还是麦瑞克,统称为人类,那么作为一个人类有一些基本的属性,包括:

  1. 姓名
  2. 年龄

然后肯定还包含一些特征行为,比如每个人都会吃东西,所以有个行为就是:

这就是作为一个人最基本的属性和行为,我们写成java代码,上述所示那样,在java中就是最基本的一个类了,也就是上述代码就定义了一个人类Person出来了,另外一个简单的理解就是,你可以把这个类想象成是一个模具,这里的Person就相当于一个造人模具,通过它我们可以造出来小明,小红等等,比如我们造出来一个小明:

//新建一个人类,名字叫做小明
        Person p = new Person();
        p.name = "小明";
        p.age = 18;
        p.eating("苹果");

是不是很简单,只需要给其赋值上具体的姓名年龄和行为就行了,比如上述代码,我们通过new Person()的方式就创建出来了一个实打实的人p,只不过目前这个p还没有具体的属性和行为,给他制造一些属性和行为,比如让他叫做小明,也就是通过**p.name = “小明”**的形式,就这样,p就成了小明,小明就是p这个Person类产生的实际对象了,一个确切的名叫小明,年龄是18,拥有吃这个行为的对象了。

想象下,模具,模板,生产……

希望以上讲述可以让你清楚的理解什么是类,什么是对象。

Class对象

那我们在看Class对象,我们之前说了,反射就是拿到一个类的字节码,然后通过Class自带的一些API去操作字节码从而去做一些事情,那这个Class对象是啥呢?

我们再来看段代码:

 //新建一个人类,名字叫做小红
        Person p1 = new Person();
        p1.name = "小红";
        p1.age = 17;
        p1.eating("橘子");

是的,我们又创造了小红出来,然后我们再回过头来看看创建小明的代码:

//新建一个人类,名字叫做小明
        Person p = new Person();
        p.name = "小明";
        p.age = 18;
        p.eating("苹果");

不知道你们发现什么没有,你看,无论是小明还是小红,他们的产生都是通过**new Person()**来的,是不是?

之前也说过了,Person相当于一个统一的总的人类,是一个模具,通过它我们可以创作出小明小红甚至任何人,但是万变不离其宗都要通过Person这个模具来创造,我们再来看一段代码:

 public static void main(String[] args) {
        //新建一个人类,名字叫做小明
        Person p = new Person();
        p.name = "小明";
        p.age = 18;
        p.eating("苹果");

        //新建一个人类,名字叫做小红
        Person p1 = new Person();
        p1.name = "小红";
        p1.age = 17;
        p1.eating("橘子");

        System.out.println(p.getClass() ** p1.getClass());
    }

你知道  System.out.println(p.getClass() ** p1.getClass()); 的输出结果是什么吗?

怎么样?和你想的是否一样呢?那么这个getClass获取的是什么呢?其实就是一个Class对象,也就是Person这个类的Class对象,至于为什么会是true呢?你看,无论是小红的这个p1还是小明的这个p是不是都是通过Person这个类创建出来的,而无论是p.getClass()还是p1.getClass()获取的都是这个对象所属的类的Class对象,那不都是Person这个类的Class对象嘛!

延伸:Class对象是由Class这个类产生的,就如这里的p和p1都是Person对象,而Person对象是由Person类产生的。


好了,到了这里,我们清楚了类和对象,也大致了解了什么是Class对象了,那么我们再看反射,反射就是通过拿到一个类的Class对象从而去操作一些事情,而我们之前说的字节码文件,就是javac编译生成的class文件其实对应的就是一个Class对象。

那么我们怎样去拿到这个Class对象呢?

获取Class对象的方式

一般来说,我们可以通过三种方式来获取Class对象:

  1. Class a = Class.forName("完整类名+包名");
  2. Class b = 实例对象.getClass();
  3. Class c = 任何类型.class;

其中第二种我们上面已经展示过了,至于第一种其实大家也是比较熟悉的,我们分别来看下这三种:

Class.forName("完整类名+包名")

直接看代码:

 //第一种:Class a = Class.forName("完整类名+包名");
        try {
            Class a = Class.forName("com.ithuangqing.javase.reflect.Person");
            
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

使用第一种方式的时候要捕获一个异常就是有可能这个类不存在,这里直接tyr-catch一下即可。

这里需要强调一下就是这里forName方法,看得出来它是一个静态方法是通过类名直接来调用的,关于它有以下需要注意事项:

  1. 其是一个静态方法
  2. 方法参数是个字符串类型
  3. 字符串需要的是一个完整的类名
  4. 完整类名必须带有包名

ok,我们接下来看看第二种方式

实例对象.getClass();

其实这种方式我们之前已经见到过了,就是这样的:

  //新建一个人类,名字叫做小明
        Person p = new Person();
        p.name = "小明";
        p.age = 18;
        p.eating("苹果");

        Class b = p.getClass();

这种方式是通过Person类的实例对象的一个getClass方法来获取的。

任何类型.class;

接下来我们来看看最后一种,上代码:

   Class c = Person.class;

怎么样,是不是觉得代码很简洁啊,第三种其实利用了对于任何一个类型来说,它都有一个class属性,比如我们常见的String类,我们也可以通过这种方式去获取它的Class对象:

  Class s = String.class;

也就是说,在Java中任何一种数据类型,当然也是包括基本数据类型的,它们都有这个.class属性,通过这个我们就可以获取其Class对象。

获取Class对象有什么用

那么到了这里,你可能会有疑问了,我们获取了类型的Class对象之后有什么用呢?

实例化对象

我们之前如果想创建一个对象是怎么做的?是不是通过new的形式,比如这里:

 //新建一个人类,名字叫做小明
        Person p = new Person();
        p.name = "小明";
        p.age = 18;
        p.eating("苹果");

我们通过new的形式创建了一个Person对象,那么现在利用反射技术,也就是我们可以通过获得的Class对象去实例化一个对象,怎么操作的呢?

首先啊,我们在之前的Person类中添加一个无参构造方法:

public Person(){
        System.out.println("Person类的无参构造方法执行了");
    }

然后们获取Person类的Class对象:

//拿到Person类的Class对象
            Class a = Class.forName("com.ithuangqing.javase.reflect.Person");
            //调用Class对象的一个newInstance方法
            Object o = a.newInstance();
            System.out.println(o);

这里多了一步操作,就是调用了Class对象的一个newInstance方法,有什么用呢?我们看下输出:

发现Person类的无参构造被执行了,说明啥,说明这个方法实例化了一个Person对象啊,另外加入我们把Person这里的无参构造给删除了,这里还能执行吗?

实际情况是还可以的,因为对于一个类来说,如果你没有写构造函数的话,那么默认都是有一个无参构造的,但是如果这样呢?

  public Person(String s){
        System.out.println("Person类的有参构造方法执行了");
    }

我们在Person类里面添加了一个有参构造方法,但是没有写无参构造,那么这个时候在执行这个newInstance会是个什么情况呢?

看到没,实例化异常,说没有找到那个初始化方法,啥意思嘞,就是这个newInstance会调用类的无参构造来完成对象的创建,记住了,必须调用无参构造,否则出错。

在保证有无参构造的前提下,我们通过获得的Class对象调用newInstance方法就创建了一个对象。

多此一举?

到了这里,不知道会不会有人感到疑惑,这不是多此一举吗?不信你看:

Person person = new Person();

感觉反射的方式好麻烦啊!但是,我告诉你,使用new的形式是没有使用反射的方式灵活的,不信我给你演示看看。

首先呢,我们先写个配置文件叫做classinfo.properties:

这里面我们定义一个className,然后我们去读取这个文件,代码如下:

 FileReader reader = new FileReader("classinfo.properties");
        Properties pro = new Properties();
        pro.load(reader);
        reader.close();
        String className = pro.getProperty("className");
        System.out.println(className);

以上代码属于IO流方面的知识,不懂的可以针对学习,如此一来我们就得到了我们写在配置文件中的className属性了。

那么你还记得我们获取Class对象的第一种方式吗,是不是就是这样:

 Class p3 = Class.forName(className);

看到了嘛?然后我们再调用newInstance方法就可以创建这个Person对象了:

  Class p3 = Class.forName(className);
        Object o = p3.newInstance();
        System.out.println(o);

输出看看:

你看,这里是不是创建了一个Person对象,那你可能还有疑问,这怎么就灵活了呢?你别着急,接下来我们去改动这个:

我们把配置文件中的className改成Object类型,然后我们再输出看看:

可以看到,我们在没有改动任何代码知识改动了配置文件的情况下就又生成了一个Object对象,也就是我们只需要改动配置文件来生成我们想要的对象即可。

而你要知道的是,改动代码的风险要比改动配置文件的风险大得多,所以这样的做法不仅灵活而且安全性更高,你要知道new的方式可是直接写死的。

获取Filed

除了实例化对象之外,我们还可以通过Class对象来获取Filed,也就是类中的属性,首先,我们还是先来看下这个Person类:

    /**
     * 年龄
     */

    public int age;
    /**
     * 姓名
     */

    String name;

在这个Person类中我简单定义了两个属性,那么我们看如下代码:

        Class c = Class.forName("com.ithuangqing.javase.reflect.Person");
        Field[] fields = c.getFields();
        System.out.println(fields.length);

可以猜测下输出是什么?

会不会感到疑惑,这里的属性不是有两个嘛?这里怎么输出1呢?我们看下这个输出的属性是啥?

Class c = Class.forName("com.ithuangqing.javase.reflect.Person");

        //获取公开的Filed
        Field[] fields = c.getFields();

        System.out.println(fields.length);

        for (Field o : fields){
            System.out.println(o.getName());
        }

相比你已经明白了,就是这里的getFileds获取的是public修饰的属性,那有没有可以获取全部属性的呢?但若干有:

//获取所有的Filed
        Field[] declaredFields = c.getDeclaredFields();
        System.out.println(declaredFields.length);
        for (Field o : declaredFields) {
            System.out.println(o.getName());
        }

我们输出来看下:

不知道你发现没有,当我们获取到Class对象之后我们就可以通过相应的get方法去获取我们想要的一些东西,比如这里的Filed。

给Filed赋值

我们以上获取到了Filed,比如这里的name和age:

那么我们可不可以给获取到的属性复制呢?答案当然是可以的,首先我们来看,我们可以通过如下的代码来获取所有的属性:

//获取所有的Filed
        Field[] declaredFields = c.getDeclaredFields();

但是你看:

也就是说这个getDeclaredFields还有一个有参方法,显而易见,是可以获取单个特定的属性,那这里的字符串应该传入什么呢?我们看各个属性之间哪里不同,是不是属性名称,比如我们想要获取name这个属性,我们就可以这样操作:

Field name = c.getDeclaredField("name");

我们输出打印这个name看下:

你看,是不是获取到了这几个name属性,那么接下来我想给这个name进行赋值又该怎么操作呢?一般来说,给属性赋值可能会使用什么setXXX之类的方法,我们看下这个Filed有没有相应的一个方法:

的确有这样的一个方法,看参数,需要传入一个对象然后一个值,那么这里的值应该就是赋值的具体值了,而这个对象就是你要给哪个对象的属性赋值,我们现在只知道是name属性,要给name属性赋值,但是也得知道这个name属性隶属于哪个对象吧。

那么,怎么获取这个对象呢?

Object o = c.newInstance();

没有忘记这个newInstance吧,于是赋值就顺理成章了。

name.set(o,"张三");

那么赋值有了,如何读去这个值呢?想一下是不是要读取这个对象中的name属性,同样有个get方法如下:

System.out.println(name.get(o));

当然,打印结果必定是“张三”

重点Method

以上我们说了关于通过反射操作Filed,接下来将是重点,也就是Method。我们如何来操作这个Method呢?也就是类中的方法,比如我们想要得到类中的这些方法,我们其实可以这样操作:

Method[] methods = c.getDeclaredMethods();

以上代码可以得到类中的方法集合,那么我们是不是可以遍历得到每个方法的名字呢?

Method[] methods = c.getDeclaredMethods();

        for (Method method : methods) {
            System.out.println(method.getName());
        }

输出过是:

还记得Person中的这个方法吗?

如此一来我们就拿到了方法的名称啊,那么我们是不是还可以拿到方法的返回值类型呢?也就是这里的void

Method[] methods = c.getDeclaredMethods();

        for (Method method : methods) {
            System.out.println(method.getName());
            System.out.println(method.getReturnType());
        }

输出结果是:

如果要获取方法的修饰符呢?

 for (Method method : methods) {
            System.out.println(method.getName());
            System.out.println(method.getReturnType());
            System.out.println(Modifier.toString(method.getModifiers()));
        }

输出结果是:

接着,如果要继续回去方法的参数呢?可能稍微有点不一样,你看:

//获取参数,其实是获取类型
            Class[] parameterTypes = method.getParameterTypes();
            for (Class cl : parameterTypes) {
                System.out.println(cl.getName());
            }

我们获取方法中的参数,其实最主要的就是知道方法参数的类型,这里就可以通过Method提供的getParameterTypes来获取方法的参数,这里得到的是一个数组,那是因为方法参数可能有多个,打印输出:

由此可知其方法参数为一个String类型的参数。

接下来就是该思考我们如何去调用这个方法了,想一下我们一般是怎么调用方法的,是不是这样?

Person p = new Person();
        p.eating("苹果");

那使用反射的形式该如何调用呢?接下来是重点哦!

调用方法

首先我们还是获取Class对象:

  Class c = Class.forName("com.ithuangqing.javase.reflect.Person");

之后我们需要获取到需要调用的方法,怎么做呢?在此之前,我们先思考一个问题:

在Java中决定一个方法的是什么?

其实就是方法名称和形参列表,所以我们要想获取一个具体的方法需要有方法名称和形参来决定,因为方法可能会出现重载,于是我们看看如何获取这个方法:

方法名称是“eating”,然后还有一个String参数,那么我们就可以通过以下方式去获取这个方法

Method eating = c.getDeclaredMethod("eating",String.class);

以上我们就获取到了这个要调用的方法,接着我们就要去真正调用,也就是去执行这个方法了:

这里有个invoke方法就是调用的意思,要调用这个方法的话是不是还需要传入相关参数,那么这里invoke的第二个参数是一个可变长度参数,其实就是对应方法的参数了,因为参数可能有多个,而第一个Object类型呢?

想一想还缺个啥?是不是缺个对象,要知道我们目前只是通过Class对象来获取的方法,但是不知道具体是哪个对象的这个方法,所以还需要实例化一个对象出来,然后去执行invoke调用

这里需要重点解决一下这行代码:

eating.invoke(obj, "橘子");

它的意思就是调用obj对象的eating方法传入“橘子”这个参数。

要好好理解了!

然后我们输出看下:

当然我们也可以使用反射给类中属性进行赋值,具体如下:

以上结果是不是更熟悉了!

关于Constructor

接下来我们就要来看看关于反射中的Constructor了。也就是通过反射机制去调用构造方法去实例化一个对象。

先看看我们之前是如何通过反射去实例化一个对象的:

 Class c = Class.forName("com.ithuangqing.javase.reflect.Person");
        Object obj = c.newInstance();

以上这种还接的吗?它其实是去调用类的无参构造函数去实例化对象,一旦没有无参构造函数的话以上的实例化过程就会出错,比如这个样子:

我们这里添加了有参构造函数,那么在继续上述代码的实例化就会出错:

就是找不到这个无参构造,所以实例化失败,那么这个时候该如何实例化对象呢?就要用到这个Constructor,具体来看

这个时候我们可以通过调用第二个方法来获取构造函数,另外,大家还需要知道的就是对于构造函数来说,区别他们的唯一标志就是形参了,所以这里可以直接如下操作:

这里有个知识点,就是getConstructor获取的是public修饰的方法

所以如果这里是这样的:

以上就会报错,所以稳妥的做法是如下:

当我们得到有参构造函数之后同样的也是调用newInstance方法,只不过这里要传入相应的参数了,执行输出:

如此一来,我们就实现了有参构造函数下的对象实例化!

反射机制的魅力

通过以上反射的学习,不知道你发现没,反射的确很灵活,灵活的原因是它的很多操作都可以通过配置读取的方式来实现,如此一来,我们就可以通过只修改配置文件的情况下去创建不一样的对象,从而高效的执行一些事情。而这一切真的是只需要修改配置而不需要修改java代码的!

基于此,我们学习了Java中关于反射的相关知识点,我相信,今天学完这些知识以后,过不了多久你就会遗忘,因为你可能将会有很长一段时间不会使用到反射,对于反射,我们主要会在以后深入学习框架的时候会用到,平常啥的,用到反射的机会基本上很少。

但是,反射确实一个不可或缺的重要知识,因为你以后想要拔高自己的技术能力,那你就得去研究框架,研究底层源码,如果你对反射不了解的话,你是很难读懂框架源码的。

所以,反射地位特殊,需熟练掌握!以备日后不时之需!



往期文章:

JDK 8 Stream 数据流效率怎么样?

王炸!!微软终于对 JDK 下手了…

轻松理解Java面向对象


欢迎加我微信,一起交流学习

如果能给个在看那就更好了,转发是最大的支持!

浏览 49
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报