反射

反射应用的非常广泛,平时几乎是天天在用了,不信?平时用 IDE 工具时最爱的操作就是输入一个类或者对象引用的时候通过.,就可以看到 IDE 工具这个类提供的方法和属性了,这个功能就是利用了反射。

比如 Struts2 和 Hibernate 这种框架就用的及其普遍,通常都是在 .xml 这种配置文件中添加框架所需要的配置信息,框架运行时将配置文件加载到内存中后,读取相应信息到对象中进行存储,最后利用反射技术动态创建所需要的对象。参考这篇反射文章

类的加载

此前在学习 JavaSE 部分的时候,讲到对象的创建过程时,第一步做的就是将该类的字节码文件 .class 加载到内存中。JVM 在加载一个类的字节码到内存中时,同时还会给这个类创建一个 java.lang.Class 类型的反射对象。也就是说并不是只有通过Class.forName()类名.class或者Object.getClass()这三种方式才会创建反射对象,其实类加载到内存的时候反射对象就已经在堆区存在了,只是只有通过这三种方式才能够得到它的引用作为返回值返回而已。

到底该如何理解 java.lang.Class 这个类型呢?通常讲类是一类对象的抽象,但是类同样也是一种抽象的概念,类也有一些共同特征,如果把它们抽象提取出来就可以用 java.lang.Class 类表示它们。看起来有点绕,这不就是所谓元数据的概念吗?所谓元数据就是描述数据的数据,这个 Class 就是描述类的类呗。参考元数据介绍

抽象理解一下,之所以把对于类的抽象用 Class 这个名字来表示,其实也是遵从命名规范。每一个 .java 源文件都被编译成一个 .class 的字节码文件,那起一个什么样的名字来抽象代表这一类的 .class 字节码文件呢?那就直接命名一个 java.lang.Class 类型的类啊。也就是说 JVM 管理的内存区域中,每一个 .class 文件都有一个被称之为反射对象的对象和它一一对应。

打个比方,.class 字节码文件就好像是一个脱光了衣服的 Java 类,反射对象就代表了这个 .class 文件的句柄,通过这个反射对象可以看到它身体的每一部分,也就是能够得到这个类的所有信息。

总结就是:对象的抽象是类,类的抽象就是 Class 。每一个字节码文件都有一个唯一的反射对象和它对应,通过这个反射对象能够得到这个字节码文件的所有信息。

反射_第1张图片
_反射.png

创建反射对象的三种方法

一个类可以有无数个对象,如果内存装得下的话。但是一个类只能有一个反射对象,因为一个类的字节码文件也只会被加载一次,这可以通过==操作符对得到的映射对象进行验证。

反射对象调用类的构造方法创建的对象是 Object 类型的,其实这也很正常啊,它也没法事先指定数据类型啊。

反射对象的创建流程:拿到类的全名,加载类对应的字节码文件,如果有静态代码块就会先执行静态代码块的内容,接着立即创建一个 Class 类型的反射对象,也叫作映射对象。

通过反射技术创建对象,会额外消耗更多资源,所以只有在需要动态创建对象的时候才用。而且通过反射技术创建对象,会忽略权限检查,能够通过反射对象直接使用私有属性和方法,破坏了封装性,还是不要随便乱用的好。

第一种:Class.forName(String className)

第一种方式通过 java.lang.Class 类的静态方法forName(String className)来加载类的字节码文件并得到反射对象的引用。反射对象也是一个对象啊,它和普通的其他对象并没有很大不同。

//第一种方式:通过加载类取得与该类对应的反射对象
Class c = Class.forName("java.util.Date");
Object obj = c.newInstance();

第二种:通过类的 class 属性得到反射对象

这种方式的特殊之处在于,加载类的字节码文件时,没有进行任何初始化,连静态代码块内容都没有执行,当然反射对象还是创建了的。

//第二种方式:通过类的class属性取得与该类对应的反射对象
Class c2 = Employee.class;
Object o = c2.newInstance();

第三种:通过对象的 getClass() 方法

此方法很明显,是直接返回堆区已经存在反射对象的引用。

java.util.Date date = new java.util.Date();
Class c = date.getClass();

方法反射

在程序编写阶段不知道要调用对象的哪个方法时,可以使用方法反射技术等到程序运行阶段再来执行具体的方法。

比如 struts1 中的 DispatchAction 类就是专门设计来避免应用中添加过多的 Action 类的。原来是一个请求对应一个Action,这样导致配置量很大,Action类过多。现在一个请求对应 DispatchAction 子类中的一个方法,只需要定义一个类来继承 DispatchAction 类并提供和请求对应的方法即可。完成此功能,就是通过解析 url ,从中得到包含的方法名,最后通过方法反射技术来调用对应的方法实现的。

方法反射具体步骤

  • 得到反射对象
  • 通过反射对象调用Class. getDeclaredMethod(String name, Class... parameterTypes)方法返回一个代表方法的 Method 类对象
    • String 类型的形参 name 表示方法名
    • 第二个形参 parameterTypes 是 Class 类型的可变参数,它接收该方法的参数列表的反射对象表示,比如 String.class,Integer.class 当形参过多时,可以将其存储在一个数组上,然后以数组的形式传递进来。可变参数本质上就是一个数组!
    • 只有明确方法名和参数列表才能确定具体是哪个方法,所以这里要传递这样的参数进去。
  • 通过 Method 类的invoke(Object obj, Object... args)方法来执行它代表的这个方法,返回的是此方法返回值的 Object 类型的表示。
    • 第一个形参 obj 表示一个实例对象,也就是说通过方法反射技术调用的是这个实例对象的方法。必须要加上它,因为方法不能脱离对象而存在的。
    • 第二个形参 args 是 Object 类型的可变参数,它表示被调用方法的实参。调用一个实例对象的方法肯定要给它传参啊!
    • 只有明确调用的是哪个实例对象的方法并给它传参,才能成功调用该方法

疑问:invoke()方法返回的是一个 Object 类型的一个对象,那如果需要调用的是一个会返回数组或者集合的方法呢?

回答:当时脑子短路了吗?Object 是任何数据的基类啊,难道它不能够代表数组或者集合吗?只是需要自己手动的去转型而已,所以反射通常都是和接口组合使用的。这也体现了 Java 所倡导的面向接口编程的思想!

总结一下,实现方法反射需要三要素,分别是:方法名、调用方法的这个实例对象以及调用方法参数的信息。

下面以 java.util.Date 类的hashCode()方法为例来展示下,方法反射的编写过程。不需要给可变参数传值时时,什么都不传就可以了。

public static void main(String[] args) throws Exception {
        String className = "java.util.Date";
        String methodName = "hashCode";
        //得到反射对象
        Class clazz = Class.forName(className);
        //通过反射对象创建实例对象
        Object obj = clazz.newInstance();
        //通过反射对象得到代表方法的Method对象
        Method method = clazz.getDeclaredMethod(methodName);
        //通过Method对象调用invoke方法来执行指定的实例对象的方法
        int hashCode = (Integer)method.invoke(obj);
        System.out.println(hashCode);
        //输出2076132911
    }

你可能感兴趣的:(反射)