「Java」反射
通过Class
实例获取class
信息的方法称为反射。通过反射,即便我们对某个实例一无所知,但仍可以在程序运行期间获取该对象的所有信息。
Class类
在java中,除了int
等基本类型,其他类型均为class
(包括interface
)。而class
是由JVM在执行过程中动态加载的。(动态加载:JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载)
JVM在第一次读取到一种class
类型时,将其加载进内存。每加载一种class
,JVM就为其创建一个Class
类型的实例,这个实例就包含了该class的所有信息(name
、package
、super
、field
、method
…)。当编译一个新类时,会产生一个同名的 .class
文件,该文件内容保存着 Class 对象。
注意Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序无法创建。
那么如何获取class
实例?
- 通过一个
class
的静态变量class
获取:
1 | Class cls = String.class; |
- 通过实例变量提供的
getClass()
方法获取:
1 | String s = "Hello"; |
- 根据完整类名获取:
1 | Class cls = Class.forName("java.lang.String"); |
此外,JVM为每一种基本类型如int
也创建了Class
,通过int.class
访问。数组(例如String[]
)也是一种Class
,它的类名是[Ljava.lang.String
。
获取Class
实例之后,也可以通过该实例创建对应类型的实例:
1 | Class cls = String.class; |
获取父类Class
1 | Class n = Integer.class.getSuperclass(); // class java.lang.Number |
获取实现的接口
1 | Class[] is = Integer.class.getInterfaces(); |
注意并不包括其父类实现的接口类型
获取接口的父接口要用getInterfaces()
,而不是getSuperclass()
判断继承关系
对两个Class
实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
:
1 | Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number |
Class 和 java.lang.reflect
一起对反射提供了支持,java.lang.reflect
类库主要包含了Field
、Method
、Constructor
Field类
通过Class
实例获取Field
实例:
Field getField(name)
:根据字段名获取某个public的field(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个field(不包括父类)Field[] getFields()
:获取所有public的field(包括父类)Field[] getDeclaredFields()
:获取当前类的所有field(不包括父类)
1 | Class stdClass = Student.class; |
通过Field实例可以获取字段信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,是一个int
,不同的bit表示不同的含义。
1 | Field f = String.class.getDeclaredField("value"); |
通过Field实例读取/设置某个对象的字段:
1 | Object p = new Person("Xiao Ming"); |
如果因为修饰符为private
或protected
而不能访问,需要先设置为f.setAccessible(true);
设置字段值通过Field.set(Object, Object)
实现:
1 | f.set(p, "Xiao Hong"); |
Method类
类似的,
获取Method
实例:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
通过Method
对象查看一个方法的所有信息:
比
Field
多了一个:getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;
调用方法:
普通方法,例如
s.substring(6);
,可以(String) m.invoke(s, 6);
静态方法,如
Integer.parseInt("1234")
,可以(Integer) m.invoke(null, "1234");
非public方法,需要设置
Method.setAccessible(true)
允许其调用
多态:
反射调用也遵循多态,即总是调用实际类型的覆写方法
1 | class Student extends Person ... |
Constructor
调用类的public无参数构造方法创建实例:
1 | Person p = Person.class.newInstance(); |
而通过获取Constructor对象,就可以调用任意构造方法。
获取Constructor的方法:
getConstructor(Class...)
:获取某个public
的Constructor
,括号内为参数;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
调用:
1 | Constructor cons1 = Integer.class.getConstructor(int.class); |
反射是一种非常规的用法,会破坏对象的封装。使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)
可能会失败。如果JVM运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java
和javax
开头的package
的类调用setAccessible(true)
,这样可以保证JVM核心库的安全。
优缺点
优点:
- 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
- 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
- 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
缺点:
- 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
- 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
动态代理
动态代理(Dynamic Proxy)机制:可以在运行期动态创建某个interface
的实例。
看个例子吧,身为一个coder,我们要会code
,也要会debug
:
1 | public interface Developer { |
而程序员有很多种呀,Java程序员,C++程序员,摸鱼程序员,带薪拉*的程序员:
1 | public class JavaDeveloper implements Developer { |
假如现在有上千个实现类,忽然有一个需求,每当一个行为产生时(调用了方法),要给coder们的行为做记录来决定年底奖金,code和debug的时候可以加kpi,摸鱼就不能加。怎么办呢,要我们在这么多实现类的接口方法中一个一个去加纪录代码吗?除了删库跑路,我们还有一种选择:动态代理。
1 | JavaDeveloper p1 = new JavaDeveloper("p1"); |
通过Proxy
创建代理对象,然后将接口方法“代理”给InvocationHandler
完成:
定义一个
InvocationHandler
实例,它负责实现接口的方法调用;通过
Proxy.newProxyInstance()
创建interface
实例,它需要3个参数:- 使用的
ClassLoader
,通常就是接口类的ClassLoader
; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler
实例。
- 使用的
将返回的
Object
强制转型为接口。
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程。
Proxy.newProxyInstance()
通常我们先生成一个实例对象,然后用Proxy的newInstance方法对这个实例对象代理生成一个代理对象。这是代理模式思想的体现,可以回顾下设计模式那块。
1 | public static Object newProxyInstance(ClassLoader loader, |
ClassLoader是负责加载类的对象,如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的效据:一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。
每个 Class 对象都包含一个对定义它的 ClassLoader 的引用。
应用程序需要实现Classloader的子类,以扩展Java虚拟机动态加载类的方式。
loder和interfaces基本就是决定了这个类到底是个怎么样的类。
InvocationHandler
InvocationHandler
作用就是,当代理对象的原本方法被调用的时候,会绑定执行一个方法,这个方法就是InvocationHandler
里面定义的内容,同时会替代原本方法的结果返回。
使用场景
在Spring项目中用的注解,例如依赖注入的@Bean、@Autowired,事务注解@Transactional等都有用到,换言之就是Srping的AOP(切面编程)。
这种场景的使用是动态代理最佳的落地点,可以非常灵活地在某个类,某个方法,某个代码点上切入我们想要的内容,就是动态代理其中的内容。