反射
Java反射
反射:程序在运行期(Runtime)可以拿到一个对象的所有信息。通常调用一个对象的方法,或访问一个对象字段,需要传入对象实例的时候使用
比如下面这个例子:
// Main.java
import com.learnjava.Person;
public class Main {
String getFullName(Person p) {
return p.getFirstName() + " " + p.getLastName();
}
}
class Person() {
public final String firstName;
public final String lastName;
}
但是,如果我们无法获取Person类,只有Object实例,或者不确定对象类型,如何获取其方法/字段?
String getFullName(Object obj) {
return ???;//无法获取类型
}
怎么办?有童鞋会说:强制转型啊
但是又要回到前提这个对象是什么类的?
于是,我们回到问题本质,除了int等基本类型外,Java的其他类型全部都是class(包括interface)引用类型。例如:
- String
- Object
- Runnable
- Exception
…
仔细思考,我们可以得出结论:class(包括interface)的本质是数据类型(Type)。
无继承关系的数据类型也无法赋值:
⚠️ 注意:
下面的例子使用了Double的Constructor,但是新版本已经不再使用了。
Number n = new Double(123.456); // OK
String s = new Double(123.456); // compile error!
类(class)加载机制
class是依靠JVM在执行过程中动态加载的,JVM第一次读取一种class类型(如String.class)的时候,将其加载到内存。
动态加载:JVM在执行Java程序时,并不是一次性把所有用到的类加载到内存,而是第一次用到该的位置才加载该类
每加载一种class,JVM就为其创建一个Class类型的实例
比如下面这个例子:
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(String name)方法时,才会加载Person.class,如果该create方法不执行,那么Person.class就不会被加载到内存
⚠️注意:Class类型也是一种类
其定义如下:
public final class Class {
private Class() {...}
}
以String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:
Class cls = new Class(String);
但是这个加载机制只发生在JVM内部,也只有JVM能创建Class实例,而且每个类在JVM中唯一创建一个Class实例,因为Class的构造方法是private的。
JVM持有这些实例,并且这些实例都指向一个数据类型,而Class实例中具体又包含了这个类的完整信息
在Class实例中保存了这个类实例化后的所有信息,包括:
- 类名 name
- 包名 package
- 父类 super
- 实现的接口 interface
- 字段 field
- 方法 method
而通过这个Class实例获取对应的类信息就是反射
获取类对应的Class实例
1. 通过这个类的静态变量获取
Class cls = String.class;
2. 通过对象的getClass方法获取 (该方法最常使用)
//比如有一个对象 a
Class cls = a.getClass();
3. 如果知道class的完整类名
调用Class的静态方法forName
Class cls = Class.forName("java.util.HashMap");
由于Class实例在JVM中是唯一的,所以上述方法对于同一个类而言,获取的实例都是同一个实例。从而可以用==
号来进行比较
⚠️注意:
Class实例比较和用instanceof的差别:
Integer i = new Integer(123);
boolean b1 = i instanceof Number;//true,因为i是Number类型的子类
boolean b1 = i instanceof Integer;//true,因为i是Integer类型
boolean b3 = i.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = i.getClass() == Number.class; // false,因为Integer.class!=Number.class
通常情况下,我们应该用instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。
使用Class实例获取类的基本信息
public class Main {
public static void main(String[] args) {
printClassInfo(Runnable.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());//打印是否为Enum
System.out.println("is array: " + cls.isArray());//打印是否为Array
System.out.println("is primitive: " + cls.isPrimitive());//打印是否为基本数据类型
}
}
输出结果如下:
Class name: java.lang.Runnable
Simple name: Runnable
Package name: java.lang
is interface: true
is enum: false
is array: false
is primitive: false
获取字段信息
Class类提供了四种方法来获取字段
- Field getField(String fieldName): 根据字段名获取某个public的字段(包括父类字段)
- Field getDeclaredField(String fieldName): 根据字段名获取当前类的某个字段,可以利用这个方法获取private的字段
- Field[] getFields():获取所有public的字段(包括父类)
- Field[] getDeclaredFields 获取当前类的所有field【含有private】(不包括父类)
字段对象Field:包含了该字段的所有信息,并且提供了获取方法
Field字段对象与它的来源类对象中的字段是一一对应的,这一点很重要,在后续使用Field的实例方法set(Object obj,Object value)传入对象名和值的时候容易出错
- String getName()获取字段名称
- Class<?> getType返回的是字段所属类型
- int getModifiers()返回字段的修饰符,是一个int类型,可以通过传入Modifier的静态方法isPublic(int modifier), isPrivate,…等一系列方法进行查看其修饰符类型
下面的代码演示了这一过程
package reflection;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class DemoReflectGetField {
public static void main(String[] args) throws NoSuchFieldException{
Class cls = Student.class;
// 返回public的父类/当前类的字段类型 以及 字段名称
// 返回继承的字段 "name"
System.out.println(cls.getField("name"));
System.out.println(cls.getField("score"));
System.out.println(cls.getField("grade"));
// 返回private字段"grade"
System.out.println(cls.getDeclaredField("rank"));
/**
* public final class String {
* private final byte[] value;
* }
* 现在利用reflection查找该字段value的信息
*/
Field f = String.class.getDeclaredField("value");
System.out.println(f.getName());
System.out.println(f.getType());class [B 表示value是一个byte[]类型
int m = f.getModifiers();
//m本身是一个无意义的值,只有传入了Modifier的静态方法才可以看到其对应的类型
System.out.println(m);
System.out.println(Modifier.isFinal(m));
System.out.println(Modifier.isPrivate(m));
}
}
class Person {
public String name;
}
class Student extends Person {
public int score;
public char grade;
private int rank;
}
输出结果如下:
public java.lang.String reflection.Person.name
public int reflection.Student.score
public char reflection.Student.grade
private int reflection.Student.rank
value
class [B
18
true
true
Process finished with exit code 0
更进一步,我们需要的其实是在不知道对象的类型的时候获取到对象的对应字段或者方法。于是Field对象就派上了大用场:
比如有一个Person2类的实例p,我们可以获取到它的所有类字段信息,再去通过字段信息获取到这个实例的值。
⚠️ 这里要抛出NoSuchFieldException和IllegalAccessException:否则会得到这两个异常,无法正常运行。
⚠️ 另外如果访问了protected, default, private修饰的变量,首先要调用setAccessible(true),否则无法绕过访问权限,也就无法访问ID
package reflection;
import java.lang.reflect.Field;
public class DemoReflectGetObjectFieldValue {
//在Person类这里ID是一个private变量
public static void main(String[] args) throws Exception {
Person2 p = new Person2("Richard");
p.setID(123);
Class cls = p.getClass();
Field f = cls.getDeclaredField("ID");
f.setAccessible(true);//但是这个ID 本身是private修饰的,因此要设置忽略修饰符,允许访问
Object value = f.get(p);
System.out.println(value);
}
}
class Person2 {
public String name;
private int ID;
public Person2(String name) {
this.name = name;
}
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
}
Why Reflection?
有童鞋会问:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?
答案是正常情况下,我们总是通过p.name来访问Person的name字段,而ID是private我们需要getID方法来进行访问,编译器会根据public、protected和private决定是否允许访问字段,这样就达到了数据封装的目的。
而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值,设置字段的值。
public class Main {
public static void main(String[] args) throws Exception {
Person2 p = new Person("Richard");
System.out.println(p.getName()); // name = "Richard"
Class c = p.getClass();
Field f1 = c.getDeclaredField("name");
f1.set(p, "Xiao Hong");
System.out.println(p.name); // name = "Xiao Hong"
Field f2 = c.getDeclaredField("ID");
f2.setAccessible(true);
f2.set(p, 234);
System.out.println(p.getID());//ID = 234
}
}
class Person2 {
public String name;
private int ID;
public Person2(String name) {
this.name = name;
}
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
}