简明扼要的反射入门教程

反射

反射作为RTTI语言(比如Java)的基础之一被很多人所熟知,但是有些同学对反射本身还是懵懵懂懂的,不是很清楚它到底有什么用。今天这节课我们就对反射本身来一个通体的认知。

定义

反射所在的包为:java.lang.reflect

它的英文版定义是:Reflection allows programmatic access to information about the fields, methods and constructors of loaded classes。the use of reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.

By default, a reflected object is not accessible.

Setting the accessible flag in a reflected object permits sophisticated applications with sufficient privilege, such as Java Object Serialization or other persistence mechanisms, to manipulate objects in a manner that would normally be prohibited.

Java反射主要是指程序可以访问或者修改它本身状态或行为的一种能力,是Java被视为动态(或准动态)语言的一个关键性质。这个机制允许程序在运行时通过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static 等)、superclass(例如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。

PS: 因为反射机制与Class类联系紧密,所以在学习反射之前需要先了解Class类是什么。

Android文档中的反射定义:https://developer.android.google.cn/reference/java/lang/reflect/package-summary.html

作用

动态的访问、修改类的成员,可以达到使用常规手段做不到的目的。

最常见的例子:一个类有一个私有的成员属性,无法通过正常的手段(比如Get方法)获取这个属性的值,那么就需要通过反射来获得它的值,

反射多用于框架和组件,通过反射可以写出复用性高的通用程序。

比如我们所熟知的Android中的Activity就是通过反射实例化生成的。

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return (Activity)cl.loadClass(className).newInstance();
    }

最后这行代码通过字符串形式的类路径加载指定的Activity类到内存中,然后通过反射的newInstance()实例化Activity对象,最后通过向下转型返回给调用者。

上面这段代码位于android.app.Instrumentation内。同理,我们所熟知的Application,Service,ContentProvider,BroadcatReceiver也是通过这种方式生成的。

Java多态的伟大之处就从这里开始!

你可能会有疑惑,为什么不直接new呢?

如果是new方法,那么new只能实例化指定的类,也就是说,如果使用new,Android系统框架只能实例化某个Activity了。而如果通过反射,那么只要是Activity的子类,都可以通过此方法实例化,这也就是多态的精髓。

优点

反射涉及到了动态与静态的概念:

  • 静态编译:在编译时确定类型,绑定对象,即通过。
  • 动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了java的灵活性,体现了多态的应用,用以降低类之间的藕合性。

反射机制的优点是可以实现动态创建对象以及修改对象的结构,体现出很大的灵活性。

缺点

它的缺点是对性能有影响。使用反射基本上是一种解释操作。由于用于字段和方法接入时反射要远慢于直接代码,反射在性能上会有所影响,但性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。即使测试中最坏情况下的计时图显示的反射操作只耗用几微秒。仅反射在性能关键的应用的核心逻辑中使用时性能问题才变得至关重要。所以,合理的使用反射将大大提高我们程序的通用性和复用性。

技术解析铺垫

运行时类型识别(Run-time Type Identification, RTTI)主要有两种方式,一种是我们在编译时和运行时已经知道了所有的类型,另外一种是功能强大的”反射”机制。

要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的,这项工作是由”Class对象”完成的,它包含了与类有关的信息。类是程序的重要组成部分,每个类都有一个Class对象,每当编写并编译了一个新类就会产生一个Class对象,它被保存在一个同名的.class文件中。在运行时,当我们想生成这个类的对象时,运行这个程序的Java虚拟机(JVM)会确认这个类的Class对象是否已经加载,如果尚未加载,JVM就会根据类名查找.class文件,并将其载入,一旦这个类的Class对象被载入内存,它就被用来创建这个类的所有对象。一般的RTTI形式包括三种:

1.传统的类型转换。如”(Apple)Fruit”,由RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常。

2.通过Class对象来获取对象的类型。如

    Class c = Class.forName("Apple");
    Object o = c.newInstance();

3.通过关键字instanceof或Class.isInstance()方法来确定对象是否属于某个特定类型的实例,准确的说,应该是instanceof / Class.isInstance()可以用来确定对象是否属于某个特定类及其所有基类的实例,这和equals() / ==不一样,它们用来比较两个对象是否属于同一个类的实例,没有考虑继承关系。

基本用法

以下分别展示了反射的基本用法:

类的获取方式

针对我们所知的不同情况分别有3种方法获取Class对象

  • 当已知类名的时候,通过”类名.class”获得

  • 当已知对象的时候,通过”对象.getClass”获得

  • 当已知包括包名在内的完整类名(假设为String格式)的时候,可通过”Class.forName(classPath)”或者”ClassLoader.loadClass(classPath)”获得

比如我们有一个类,类的结构如下:

package com.sahadev;

/**
 * Created by Sahadev on 2017/4/27.
 */

public class ClassABean {

    public boolean mFlag;

    private IMethod mIMethod;

    public ClassABean() {
    }

    public ClassABean(boolean mFlag, IMethod iMethod) {
        super();
    this.mFlag = mFlag;
    this.mIMethod = iMethod;
    }

    private void printBValue(){
    System.out.println("The mFlag = " + mFlag);
    }
}

那么类ClassABean的字节码的获取方式有以下3种:

  • ClassABean.class;
  • new ClassABean().getClass();
  • Class.forName(“com.sahadev.ClassABean”);或者ClassLoader.loadClass(“com.sahadev.ClassABean”);

获取到Class字节码对象之后,我们就可以对其进行操作了。

通过无参构造方法实例化对象

通过无参构造的方式有两种,一种是我们上面看到的,使用newInstance()方法,而另一种是获得类的无参构造方法,然后通过无参构造方法创建对象。其中newInstance()方法默认调用的是无参构造方法,如果类没有无参构造方法,则会有异常抛出。

这两种方法的使用方式分别如下:

    //通过newInstance()方法构造
    ClassABean instanceA = ClassABean.class.newInstance();

    //通过无参构造方法构造
    Constructor<ClassABean> constructor = ClassABean.class.getConstructor();//获取无参构造方法
    ClassABean instanceB = constructor.newInstance();//实例化

通过有参构造方法实例化对象

通过有参构造方法实例化对象的方法如下:

    Constructor<ClassABean> constructor = ClassABean.class.getConstructor(Boolean.class, ClassBBean.class);//获取指定参数的构造方法
    ClassABean instanceB = constructor.newInstance(true, new ClassBBean());//通过对象参数实例化对象

上面的代码等价于:

    ClassABean instanceB  = new ClassABean(true, new ClassBBean());

通过以上有参构造方法构造的对象,它们的成员属性现在都已经被赋了值。其中属性mFlag的值为true,属性mIMethod的实际实现者为ClassBBean。

PS: 在我们的示例中提到的ClassBBean类与ClassCBean类都同样实现了IMethod接口。

方法调用

从以上的示例中我们知道了如何通过反射来实例化一个对象,接下来我们通过反射来调用一下类的私有方法。

在ClassABean类中提供了一个私有方法printBValue(),我们看看如何通过反射来调用这个方法:

    Method method = ClassABean.class.getDeclaredMethod("printBValue");

    ClassABean instanceB = new ClassABean(true,new ClassBBean());
    method.setAccessible(true);
    method.invoke(instanceB);

控制台会正确输出我们预想中的值:

The mFlag = true

这样调用和我们通过普通方法调用的效果是一致的,只是反射可以调用类的私有方法。

在这里细心的同学就会发现,Class类本身提供了两个获取方法的方法,一个是getDeclaredMethod,另一个是getMethod。那这两者有什么区别呢?getDeclaredMethod用于获取所有的方法,包括私有方法。而getMethod则用于获取public方法,其它权限方法无法获得。

属性获取与赋值

属性的获取与方法类同:

    Field flagField = ClassABean.class.getDeclaredField("mFlag");
    flagField.setAccessible(true);

    ClassABean classABeanInstance = new ClassABean(true, new ClassBBean());
    boolean flag = (boolean) flagField.get(classABeanInstance);

这样就可以获得对象classABeanInstance的mFlag的值,同样的,我们还可以获得属性mIMethod的值:

    Field iMethodField = ClassABean.class.getDeclaredField("mIMethod");
    iMethodField.setAccessible(true);

    ClassABean classABeanInstance = new ClassABean(true, new ClassBBean());
    IMethod iMethod = (IMethod) iMethodField.get(classABeanInstance);

其中IMethod的具体实例为ClassBBean对象。

接下来我们演示一下如何替换属性的值,这种方式在很多地方都很常见,它的用途很广:

    Field iMethodField = ClassABean.class.getDeclaredField("mIMethod");
    iMethodField.setAccessible(true);

    ClassABean classABeanInstance = new ClassABean(true, new ClassBBean());

    ClassCBean classCBean = new ClassCBean();
    iMethodField.set(classABeanInstance, classCBean);

    IMethod iMethod = (IMethod) iMethodField.get(classABeanInstance);

通过这样的方式,我们再获取mIMethod的值将会是classCBean对象。我们在这里使用了set的方法,set方法用于给指定对象的属性赋值。

用例1(修改TextView的autoLink的点击实现)

相关文章介绍:如何修改TextView链接点击实现(包含链接生成与点击原理分析)

用例2(热修复实现)

相关文章介绍:一步步手动实现热修复

扩展了解

通过反射我们可以获得一个类的注解,它的父类以及实现的接口等。了解反射可以有助于我们实现抽象能力更强的框架。

扩展阅读:https://developer.android.google.cn/reference/java/lang/Class.html

参考地址

http://c.biancheng.net/cpp/html/1781.html
http://www.voidcn.com/blog/zbuger/article/p-5771880.html
http://www.fanyilun.me/2015/10/29/Java反射原理/
http://rednaxelafx.iteye.com/blog/548536
http://blog.csdn.net/u013551462/article/details/51261817

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页