反射
什么是反射?
Java的反射是指程序在运行期可以拿到一个对象的所有信息。
目的是为了什么?
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
Class类
学习反射,首先要学习Class类。
除了int
等基本类型以外,Java的所有其他类型全部都是class
(包括interface
)。
class
是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class
类型时,将其加载进内存。
每加载一种class
,JVM就为其创建一个 Class
类型的实例,并关联起来。
public final class Class {
private Class() {}
}
以String
类为例,当JVM加载String
类时,它首先读取String.class
到内存,然后为String
类创建一个Class
实例并关联起来:
Class cls = new Class(String);
这个 Class
实例是JVM内部创建的,JDK源码中Class
类的构造方法是 private
,只有JVM能创建 Class
实例。
所以,JVM持有的每个 Class
实例都指向一个数据类型(class
或interface
):
一个Class
实例包含了该class
的所有完整信息:
由于JVM为每个加载的class
创建了对应的 Class
实例,并在实例中保存了该 class
的所有信息,包括类名、包名、父类、实现的接口、所有的方法、字段等,因此,如果获取了某个 Class
实例,我们就可以通过这个 Class
实例获取到该实例对应的 class
的所有信息。
这种通过 Class
实例获取 class
信息的方法,称为反射(Reflection)。
如何获取一个 class
的 Class
实例?有三个方法:
方法一:直接通过一个 class
的静态变量 class
获取:
Class cls = String.class;
方法二:如果我们有一个实例变量,可以通过该实例变量提供的 getClass()
方法获取:
String s = "Hello";
Class cls = s.getClass();
方法三:如果知道一个class
的完整类名,可以通过静态方法 Class.forName()
获取:
Class cls = Class.forName("java.lang.String");
因为Class
实例在JVM中是唯一的,所以上述方法获取的 Class
实例是同一个实例。可以用==
比较两个Class
实例。
注意一下Class
实例比较,和instanceof
的差别:
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类
boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
用instanceof
不但匹配指定类型,还匹配指定类型的子类。而用==
判断class
实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用instanceof
判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个class
的时候,我们才使用==
判断class
实例。
要从Class
实例获取基本信息,参考以下的代码:
public class Main {
public static void main(String[] args) {
printClassInfo("".getClass());
printClassInfo(Runnable.class);
printClassInfo(java.time.Month.class);
printClassInfo(String[].class);
printClassInfo(int.class);
}
static void printClassInfo(Class cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
}
}
注意到数组(例如String[]
)也是一种class
,而且不同于String.class
,它的类名是[Ljava.lang.String
。此外,JVM为每一种基本类型,如int也创建了Class
,通过int.class
访问。
如果获取到了一个Class
实例,我们就可以通过该Class
实例来创建对应类型的实例:
// 获取String的Class实例
Class cls = String.class;
// 创建一个String实例
String s = (String)cls.newInstance();
上述代码相当于new String()
。通过Class.newInstance()
可以创建类实例,它的局限是:只能调用public
的无参数构造方法。带参数的构造方法,或者非public
的构造方法都无法通过Class.newInstance()
被调用。
动态加载
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。例如:
// Main.java
public class Main {
public static void main(String[] args) {
if (args.length > 0) {
create(args[0]);
}
}
static void create(String name) {
Person p = new Person(name);
}
}
当执行Main.java
时,由于用到了Main
,因此JVM首先会把Main.class
加载到内存。但是,并不会加载Person.class
,除非程序执行到create()
方法,JVM发现需要加载Person
类时,才会首次加载Person.class
。如果没有执行create()
方法,那么Person.class
根本就不会被加载。
这就是JVM动态加载class
的特性。
动态加载class
的特性对于Java程序非常重要。利用JVM动态加载class
的特性,我们才能在运行期根据条件加载不同的实现类。例如 Commons Logging 总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。例如JVM动态加载特性,大致的实现代码如下:
// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
factory = createLog4j();
} else {
factory = createJdkLog();
}
boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (Exception e) {
return false;
}
}
这就是为什么我们只需要把Log4j的jar包放到classpath中,Commons Logging就会自动使用Log4j的原因。
小结
JVM为每个加载的class
及interface
创建了对应的Class
实例来保存class
及interface
的所有信息;
获取一个class
对应的Class
实例后,就可以获取该class
的所有信息。
通过Class实例获取class
信息的方法称为反射(Reflection);
JVM总是动态加载class
,可以在运行期根据条件来控制加载class。