《深入理解Java虚拟机》 类 & Class对象/反射
在类加载成功之后,便以Class对象的形式保存在内存中。可以将Class对象理解成实例对象的元数据,通过Class对象中描述的类型信息,便可以创建对应的实例对象,从而进行方法调用,以及字段修改。那么,便可以通过反射根据运行时获取的类型信息来创建实例,而且可以跳过访问权限的限制访问字段和方法
1. Class
Class对象被用来描述某种类型,但是类型有很多种,那么如何区分描述的是哪种类型?其实觉得这里可以将Class对象理解成一个类型属性的容器,其中包括了构造器Constructor
,字段Field
,方法Method
,注解等信息,而至于是哪种类型,则可以通过泛型来表示
- 关于Class对象的获取
如果在编译期间类型已知,则可以直接通过类型字面量获取,比如
Class<Object> clazz = Object.class;
,不过使用类型字面量的方式并不会触发Class对象的初始化,它被延迟到首次对静态域,或者静态方法的引用(构造器也是静态调用)如果已经存在类型对应的实例,则可以直接通过实例调用
Object
提供的getClass()
方法,因为在使用类型信息创建对象时,会在对象头中(直接指针的方式)保留指向类型信息的引用如果上面信息都没有,那么可以通过类的全限定名由类加载器进行加载,Class提供了接口
Class.forName()
,由其帮忙调用加载器进行加载,当然也可以自己主动指定加载器。但是,这里的加载就会立即进行初始化了(如果没有加载过)。另外,对于加载器而言,它无法知道加载的是那种具体的类型信息,只知道是某种确定的类型,因此只能用Class<?>来接收
2. 反射
反射是Java作为动态语言的手段或者体现,为Java赋予了更多的灵活性,它是很多框架(比如spring、mybatis),以及动态代理实现的基础。在Class对象中,我们可以拿到类型的种种信息,比如构造器,字段和方法等,而这些信息也各自定义了对应的类型来表示,其中定义了各自可以支持的操作和使用方式,当然这些信息类型以及Class对象自己所属的类型都由虚拟机自身所定义。
下面简单整理一些常见的反射使用场景
2.1. Constructor
最常见的就是通过反射创建实例,如果是默认构造器,则可以直接通过Calss对象提供的newInstance()
创建,否则需要获取对应的构造器,而构造器通过Constructor
来封装,Class对象提供了getConstructors()
以及getDeclaredConstructor()
接口来获取类型实例的构造器列表,区别在于获取的是public还是所有的,有了构造器之后便可以调用其newInstance(..)
来创建对应的实例
2.2. Method
一般情况下,是先有对象,然后再调用其方法,但Method提供了invoke(...)
接口,它本身表示一个方法实例,但它可以帮忙通过指定的实例来调用。同样的,Class对象也提供了getMethods()
以及getDeclaredMethods()
接口来获取方法列表
2.3. Field
对于字段也是类似的,获取到类型中定义的字段Field
之后,便可以为对应的实例进行赋值,但是要求先判断字段本身的所属类型,另外如果字段本身并不可见,则先要通过setAccessible(true)
来设置其可访问
2.4. 注解
对于类Class
、方法Method
、以及字段Field
上的注解,各自都提供了getAnnotation(XX.class)
接口来获取,注解可以简单理解成一种附属在字段或方法上的只读标记,通常这些注解信息可以表示某种处理方式获取其它含义。
2.5. 泛型
其实,相比于类型Class,还有更高层的抽象Type
,它包括原始类型、参数化类型、数组类型、类型变量以及基本类型等
1 | public interface Type { |
对于参数化类型,可以通过反射获取其类型参数,比如:
1 | public class Person<T> { |
1 | Class clazz = Person.class; |
在继承参数化类型时,可以选择保留或者使用具体类型,比如
1 | public class Man<T> extends Person<T> { |
或者
1 | public class Man extends Person<String> { |
此时可能希望通过父类获取其实际使用的参数类型
1 | Type type = clazz.getGenericSuperclass(); |
类似的,对于泛型接口的实现也一样,比如HashMap
中判断目标实例是否为Comparable
并且符号自限定类型的判断
1 | static Class<?> comparableClassFor(Object x) { |
2.6. CallerSensitive
在反射Api中,即java.lang.reflect
下面,经常会看到注解@CallerSensitive
,其目的是找到真正的调用者,但由于嵌套调用的关系,它需要忽略掉api自身的调用,因此就给自己定义的方法添加@CallerSensitive
进行标记,这样在检查调用链时发现@CallerSensitive
就直接跳过,直到发现真正的调用者。
以前的做法是检查固定深度,这样有一个问题,如果本来调用者没有访问权限,他可以修改调用链深度来骗过检查。比如:调用者->反射1->反射2 这样的调用链上,反射2检查权限时如果检查深度固定为1,那么看到的将是反射1的类,这就被欺骗了,而反射相关的类通常有很高的权限,容易导致安全漏洞。
参考:
- 《深入理解java虚拟机》