夯实基础:Java的反射

前言

为什么要写Java的反射?因为本人在阅读很多注入依赖这种开源库(类似Dragger2,Butterknife)的源码的时候,发现其代码都运用了大量的Java反射。本身Java反射就是框架的灵魂,为了能帮助更多的读者读懂这些开源库的代码,我决定开启一个系列文章,分别是:Java的反射;Java的注解;利用Java的反射和注解手撸一个Android注入依赖框架;ButterKnife源码解析。这篇文章是该系列的第一篇:Java的反射。

什么是Java的反射机制

java允许开发者在程序运行过程中操作(访问和修改)类的各种属性以及方法。注意加粗的几个字,“程序运行过程中”,那什么是Java文件的程序运行过程呢?

Java文件的程序运行过程

首先Java文件的程序运行过程分为三个阶段:

  • Source(源代码阶段)
  • Class(类对象阶段)
  • Runtime(运行时)

Source(源代码阶段)

我们先创建一个Person.java文件,再用javac命令编译Person.java文件,接着就会生成一个Person.class文件,此时这两个文件都在磁盘里面,还有被加载到JVM内存里面,这个时候就处于Source(源代码阶段)

java和class文件.png

Class(类对象阶段)

当Person.class文件被类加载器(ClassLoader)加载到JVM的内存里,此时JVM运行时的方法区里面会生成一个Class类对象Class<Person> clz,这个Class类对象非常重要,这里面包含了我们对类的描述。比如说我们现在这个Person.Java文件里有成员变量、构造方法以及成员方法

public class Person extends Object {
    /**
     * 成员变量
     * */
    public String name;
    int age;
    protected int sex;
    private int id;
    /**
     * 构造方法
     * */
    public Person(){

    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    /**
     * 成员方法
     * */
    private void eat(){
        System.out.println("eat-----");
    }
    private void drink(String drink) {
        System.out.println("drink----"+drink);
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                ", id=" + id +
                '}';
    }
}

那么Class类对象是如何描述这个Person类呢?Java的思想是一切皆对象,Class类对象对类的描述也是通过对象

  • 成员变量:用的是Field对象
  • 构造方法:用的是Constructor对象
  • 成员方法:用的是Method对象
    生成Class类对象存在JVM内存中的这个阶段就是Class类对象阶段

Runtime(运行时)

这个阶段可能大家相对就比较了解了,因为我们java程序绝大多数的情况都处于这个阶段,举个例子,当我们调用Person person = new Person();创建一个对象时,接着会在堆内存里生成一个person对象,但是这个person到底是如何被生成的呢?其实还是调用Class类对象的方法,可见Class类对象是多么重要的,而这个Class类对象也是我们实现反射的基础。

小结

这里做一个小结,因为java运行过程中这三个阶段对我们理解反射的概念非常重要,我这里画了一个图,大家理解一下


java程序运行过程的三个阶段.png

获取Class类对象

上面已经讲到了Class类对象是我们反射的基础,有了Class类对象才能实现反射,那该如何获取Class类对象呢?
java给我们提供了三种方式获取Class类对象,同时也对应上面讲的三个阶段:

  • Source(源代码阶段):Class clz = Class.forName("com.example.kaka.Person");
    ,这个方法就是通过Java文件的全限定名(包名+类名)把它的class文件加载到JVM内存里面,此时我们就能得到Class类文件,由于是源代码阶段,所以这个方法我们经常用来加载配置文件,比如说Spring在启动的时候会加载很多的配置文件,底层实现用的就是这个方法
  • Class类对象:Class clz = Person.class;这个就直接用的Person的静态属性
  • Runtime(运行时):因为是运行时了,首先我们得先有Person这个对象,Pserson p = new Person;Class clz = p.getClass();
    注意:通过以上三种方式生成的Class类对象都是相同的,也就是在内存的地址是一样的,这里如果你明白类的加载机制应该很容易理解,不明白的可以温习一下类的加载机制。

Class类对象能干什么

Class类对象是对类的描述,拿到了所有类的信息,理论上我们就什么都能干,比如说:获取类的构造方法、成员变量、成员方法、类名等等...下面我们分别讲一下

获取类的构造方法

对类的构造方法修饰的对象是Constructor对象,Class类对象里面提供了5个相关方法

  • public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
  • public Constructor<?>[] getConstructors() throws SecurityException
  • public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException
  • public Constructor<?>[] getDeclaredConstructors() throws SecurityException
  • public Constructor<?> getEnclosingConstructor()
    先聊前四个方法,前两个方法的方法名没有Declared修饰,他们只能获取到用public修饰的构造方法;后面两个方法有有Declared修饰,他们可以获取所有的构造方法,不管是private还是protected修饰的,这里我们写一段代码,打印一下log
    这里为了演示效果,我在Person类里多加了几个构造方法
 /**
     * 构造方法
     */
    public Person() {

    }

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

    private Person(int id) {
        this.id = id;
    }

    protected Person(int id, int sex) {
        this.id = id;
        this.sex = sex;
    }

打印log的代码

        Class<Person> personClass = Person.class;
        System.out.println("-----------------所有public修饰的构造方法-------------------");
        Constructor<?>[] constructors = personClass.getConstructors();
        for (Constructor constructor1:constructors) {
            System.out.println(constructor1);
        }
        System.out.println("---------------所有的构造方法---------------------");
        Constructor<?>[] declaredConstructors = personClass.getDeclaredConstructors();
        for (Constructor declaredConstructor:declaredConstructors) {
            System.out.println(declaredConstructor);
        }

看一下log的打印结果


运行结果1.png

我们再看有参数的那两个方法,他们是获取指定方法名的构造方法,接收的参数是参数类型的Class类对象;而无参的两个方法后面带了个s,也就是他们获取的是构造方法方法数组。

        Class<Person> personClass = Person.class;
        Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
        System.out.println("----------------指定参数类型的public修饰的构造方法-----------------");
        System.out.println(constructor);
        personClass.getDeclaredConstructor();

        constructor = personClass.getDeclaredConstructor(int.class);
        System.out.println("----------------指定参数类型的构造方法-----------------");
        System.out.println(constructor);
        personClass.getDeclaredConstructor();

运行结果

运行结果2.png

现在再讲一下第5个方法,getEnclosingConstructor(),官方的解释:“如果该 Class 对象表示构造方法中的一个本地或匿名类,则返回 Constructor 对象,它表示底层类的立即封闭构造方法。否则返回 null。简单来说,就是Person里面的构造方法声明了一个内部类InnerMan,此时用InnerMan的Class类对象调用getEnclosingConstructor()获取到的构造方法是Person的构造方法

 public Person() {
        class InnerMan {

        }
        InnerMan man = new InnerMan();
        Constructor<?> enclosingConstructor = man.getClass().getEnclosingConstructor();
        System.out.println(enclosingConstructor);
    }
运行结果3.png

这个方法在实际应用中比较少见,大家知道即可

用过上面的方式我们拿到了类的构造方法对象Constructor,那我们现在要干嘛?反射机制的定义是Java允许开发者在程序运行过程中**操作(访问和修改)类的各种属性以及方法。现在拿到Constructor,我们要做的操作就是创建对象。Constructor提供一个非常重要的api

  • public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
    调用这个方法我们就能得到一个对象,里面接收的是一个可变参数,如果调用无参的构造方法就什么都不传,有参的就传对应的参数就行
        Class<Person> personClass = Person.class;
        Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
        Person person1 = constructor.newInstance("张三", 24);

        constructor = personClass.getConstructor();
        Person person2 = constructor.newInstance();

这样我们就能创建Person对象了。我们再试一下private修饰的构造方法,都知道private修饰的构造方法在类的外部是不允许被调用的,但是对反射而言不区分什么内部外部,反射都可以得到,就是这么强大

        constructor = personClass.getDeclaredConstructor(int.class);
        constructor.setAccessible(true);
        Person person = constructor.newInstance(1);

注意:这里最重要的一句是constructor.setAccessible(true);,当我们访问private修饰的构造方法、成员变量、成员方法,都需要调用这个API,它的意思是取消安全检查机制,这样我们就可以调用私有的构造方法了,这一点非常重要,假如我们不加这一句代码会怎么样,我们试试

运行结果

直接就报错了,所以在访问私有属性以及方法之前,必须要加上obj.setAccessible(true);

获取类的成员变量

之前讲过对成员变量修饰的对象是Field,Class类对象里面提供了四个相关的方法

  • public Field getField(String name) throws NoSuchFieldException
  • public Field[] getFields() throws SecurityException
  • public native Field getDeclaredField(String name) throws NoSuchFieldException
  • public native Field[] getDeclaredFields();
    跟前面的构造方法类似,首先看前两个方法的方法名没有Declared修饰,他们只能获取到用public修饰的成员变量;后面两个方法有有Declared修饰,他们可以获取所有的成员变量,不管是private还是protected修饰的;我们再看有参数的那两个方法,他们是获取指定变量名的成员变量,而无参的两个方法后面带了个s,也就是他们获取的是变量数组。
       Class<Person> personClass = Person.class;
        Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
        Person person1 = constructor.newInstance("张三", 24);
        Field[] declaredFields = personClass.getDeclaredFields();//获取所有成员变量
        Field id = personClass.getDeclaredField("id");//获取指定的成员变量
        Field name = personClass.getField("name");//获取指定的public成员变量

现在我们拿到了成员变量了,对成员变量而言,我们的操作就是读取和修改,Field对象也提供了两个重要的api

  • public native Object get(Object obj) throws IllegalArgumentException, IllegalAccessException;//获取变量的值
  • public native void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException;//设置变量的值,第一个参数是对象,第二个参数是设置变量的值
        Class<Person> personClass = Person.class;
        Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
        Person person1 = constructor.newInstance("张三", 24);
//        Field[] declaredFields = personClass.getDeclaredFields();//获取所有成员变量
//        Field id = personClass.getDeclaredField("id");
        Field name = personClass.getField("name");
        Object nameVal = name.get(person1);
        System.out.println(nameVal);
        name.set(person1,"李四");
        nameVal = name.get(person1);
        System.out.println(nameVal);

这里解释一下,我们以name为例,先获取一下name的值,然后在把name改成“李四”

运行结果

控制台打印的结果符合我们的预期,同样地,当我们访问被private所修饰的成员变量也要调用obj.setAccessible(true);

        Field id = personClass.getDeclaredField("id");
        id.setAccessible(true);
        id.set(person1,3);
       //这里就不做演示了

获取类的成员方法

描述方法的对象是Method,Class类对象给我们提供了5个获取Method的相关方法

  • public Method[] getMethods() throws SecurityException:获取所有public修饰的方法
  • public Method getMethod(String name, Class<?>... parameterTypes):获取指定方法名以及参数类型的被public修饰的成员方法
  • public Method[] getDeclaredMethods() throws SecurityException:获取所有的成员方法
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException:获取指定方法名以及参数类型的成员方法
  • public Method getEnclosingMethod():如果该 Class 对象表示成员方法中的一个本地或匿名类,则返回 Method 对象,它表示底层类的立即封闭成员方法。否则返回 null。基本上跟前面getEnclosingConstructor()是类似的。
        Method[] methods = personClass.getMethods();//获取所有的public方法
        Method[] declaredMethods = personClass.getDeclaredMethods();//获取所有的方法
        Method drink = personClass.getDeclaredMethod("drink", String.class);//获取指定的方法名和参数类型的方法

拿到了方法对象Method以后,我们还是要操作,只需要调用方法就行了,对应的Method提供了一个重要的api

  • public native Object **invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException:第一个参数传具体对象,第二个传方法里的参数,没有就不传
        Method drink = personClass.getDeclaredMethod("drink", String.class);
        drink.setAccessible(true);
        drink.invoke(person1,"可乐");

访问私有方法同样要调用** drink.setAccessible(true);**

注意:关于Class对象还有其他一些比较常用的api,这里我们就不单独罗列出来了,直接放到下面:

  • public String getName():获取类的全限定名
  • public Package getPackage():获取Package对象
  • public Class<?>[] getInterfaces() :获取接口数组
  • public Annotation[] getAnnotations():获取注解
    至此,我们就通过反射的机制拿到了一个类的所有信息,并可以操作其相关信息,建议大家自己写个小demo,跑一下

总结

反射机制可以允许我们在java程序运行过程中操作类的各种属性。java程序运行过程又分为三个阶段,Source(源代码);Class(类对象);Runtime(运行时),这三个阶段又同时提供了获取Class类对象的方法,拿到了Class对象以后我们就可以操作类的属性(成员变量、构造方法、成员方法),以此来实现了反射的效果。反射很强大,它允许我们在任何地方操作类的任何信息,因此在性能略微有影响,不过影响不大,因为硬件方面的原因,10ms和70ms对我们来说是几乎没有差距。反射也是Java框架的灵魂,掌握反射有助于我们理解使用的框架,更能帮助我们开发自己的框架。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容