Java的反射
Class类
除了int等基本类型外,Java的其他类型全部都是class(包括interface)。
而class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存。
每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。
由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。
这种通过Class实例获取class信息的方法称为反射(Reflection)。
获取一个class的Class实例,方法如下图;
1.Class实例比较和instanceof的差别
用instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个class的时候,我们才使用==判断class实例。
2.获取Class实例基本信息
3.通过Class实例创建对应类
上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。例如:
当执行Main.java时,由于用到了Main,因此,JVM首先会把Main.class加载到内存。然而,并不会加载Person.class,除非程序执行到create()方法,JVM发现需要加载Person类时,才会首次加载Person.class。如果没有执行create()方法,那么Person.class根本就不会被加载。
访问字段
对任意的一个Object实例,只要我们获取了它的Class,就可以获取它的一切信息。
通过Class实例获取字段信息。
1.获取Field字段对象
Class类提供了以下几个方法来获取Field字段对象:
-
Field getField(name):根据字段名获取某个public的field(包括父类) Field
-
getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类) Field[]
-
getFields():获取所有public的field(包括父类) Field[]
-
getDeclaredFields():获取当前类的所有field(不包括父类)
2.获取Field字段对象信息
一个Field对象包含了一个字段的所有信息:
-
getName():返回字段名称,例如,“name”;
-
getType():返回字段类型,也是一个Class实例,例如,String.class;
-
getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。
3.获取Field字段对象值
利用反射除了可以拿到字段的一个Field实例,还可以拿到一个实例对应的该字段的值;
上述代码先获取Class实例,再获取Field实例,然后,用Field.get(Object)获取指定实例的指定字段的值。
运行代码,如果不出意外,会得到一个IllegalAccessException,这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get§;前,先写一句f.setAccessible(true);
调用Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。
有童鞋会问:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?
答案是正常情况下,我们总是通过p.name来访问Person的name字段,编译器会根据public、protected和private决定是否允许访问字段,这样就达到了数据封装的目的。而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。
4.设置Field字段对象值
通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。如下图;
调用方法
1.获取Method方法对象。
Class类提供了以下几个方法来获取Method:
- Method getMethod(name, Class…):获取某个public的Method(包括父类)
- Method getDeclaredMethod(name, Class…):获取当前类的某个Method(不包括父类)
- Method[] getMethods():获取所有public的Method(包括父类)
- Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
2.获取Method方法对象信息
一个Method对象包含一个方法的所有信息:
- getName():返回方法名称,例如:“getScore”;
- getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;
- getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class,
int.class}; - getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
调用构造方法
通过反射来创建新的实例,通常可以调用Class提供的newInstance()方法,但是它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
1.获取Constructor方法对象
可以通过Class实例获取Constructor对象,它包含一个构造方法的所有信息,Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例;通过Class实例获取Constructor的方法如下:
- getConstructor(Class…):获取某个public的Constructor;
- getDeclaredConstructor(Class…):获取某个Constructor;
- getConstructors():获取所有public的Constructor;
- getDeclaredConstructors():获取所有Constructor。
注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。
获取继承关系
有了Class实例,我们还可以获取它的父类的Class;
此外,对所有interface的Class调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces();
如果一个类没有实现任何interface,那么getInterfaces()返回空数组。
当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom():
// Integer i = ?
// true,因为Integer可以赋值给Integer
Integer.class.isAssignableFrom(Integer.class);
// Number n = ?
// true,因为Integer可以赋值给Number
Number.class.isAssignableFrom(Integer.class);
// Object o = ?
// true,因为Integer可以赋值给Object
Object.class.isAssignableFrom(Integer.class);
// Integer i = ?
// false,因为Number不能赋值给Integer
Integer.class.isAssignableFrom(Number.class);
动态代理
动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。
JDK提供的动态创建接口对象的方式,就叫动态代理。
在运行期动态创建一个interface实例的方法如下:
- 定义一个InvocationHandler实例,它负责实现接口的方法调用;
- 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
- 使用的ClassLoader,通常就是接口类的ClassLoader;
- 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的InvocationHandler实例。
- 将返回的Object强制转型为接口。
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:
public class HelloDynamicProxy implements Hello {
InvocationHandler handler;
public HelloDynamicProxy(InvocationHandler handler) {
this.handler = handler;
}
public void morning(String name) {
handler.invoke(
this,
Hello.class.getMethod("morning", String.class),
new Object[] { name }
);
}
}