Java 中的反射机制(两万字超全详解)
文章目录
一、反射概述
1. 什么是反射?
反射(Reflection
),Java 中的反射机制是指,Java 程序在运行期间可以获取到一个对象的全部信息。
反射机制一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。
2. 反射机制原理
反射机制允许 Java 程序在运行时调用Reflection API
取得任何类的内部信息(比如成员变量、构造器、成员方法等),并能操作类的实例对象的属性以及方法。
在Java 程序中,JVM
加载完一个类后,在堆内存中就会产生该类的一个 Class
对象,一个类在堆内存中最多只会有一个 Class
对象,这个Class
对象包含了该类的完整结构信息,我们通过这个 Class
对象便可以得到该类的完整结构信息。
这个 Class
对象就像是一面镜子,我们透过这面镜子可以清楚地看到类的结构信息。因此,我们形象的将获取Class
对象的过程称为:反射。如下图:
Java 反射机制原理示意图:
3. 反射优点和缺点
- 优点:可以
动态
地创建和使用对象,反射机制是 Java 框架的底层核心,其使用灵活,没有反射机制,底层框架就失去支撑。- 缺点:使用反射基本是解释执行,对程序执行速度有影响。
4. 类加载概述
在深入讲解反射前,先来介绍一下 Java中类的加载与反射机制。
反射机制是 Java实现动态语言的关键,也就是通过反射实现类的动态加载。
静态加载:编译时就加载相关的类,如果程序中不存在该类则编译报错,依赖性太强。
动态加载:运行时加载相关的类,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。
举个例子:
public class ClassLoad {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int key = sc.nextInt();
switch(key) {
case 0:
Cat cat = new Cat();
break;
case 1:
// 通过反射创建一个Dog 类对象,不提供代码,只是文字说明
break;
}
}
}
- 上面代码中,根据 key 的值选择创建 Cat/Dog 对象,但是在代码编译时,编译器会先检查程序中是否存在 Cat 类,如果没有,则会编译报错;编译器不会检查是否存在 Dog 类,因为 Dog 类是使用反射的方式创建的,所以即使程序中不存在 Dog 类,也不会编译报错,而是等到程序运行时,我们真正选择了 key = 1 后,才会去检查 Dog 类是否存在。
类加载的时机:
- 静态加载
- 当新创建一个对象时(new),该类会被加载;
- 当调用类中的静态成员时,该类会被加载;
- 当子类被加载时,其超类也会被加载;
- 动态加载
- 通过反射的方式,在程序运行时使用到哪个类,该类才会被加载;
类加载的过程图:
5. 类加载各阶段完成的功能
- 加载阶段:将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,此过程由类加载器完成。
- 连接阶段:又分为验证、准备、解析三个小阶段,此阶段会将类的二进制数据合并到 JRE 中。
- 初始化阶段:JVM 负责对类的
静态成员
进行初始化。
如下图所示:
5.1 加载阶段
JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、jar 包、甚至网络文件)转换为
二进制字节流
加载到内存中,并生成一个代表该类的 java.lang.Class 对象。
5.2 连接阶段——验证
5.3 连接阶段——准备
JVM 会在该阶段对静态变量分配内存并进行默认初始化(不同数据类型会有其默认初始值,如:int ---- 0,boolean ---- false 等)。这些变量的内存空间会在方法区中分配。
举例如下:
public class ClassLoad {
public static void main(String[] args) {
// 属性=成员变量=字段
// 类加载的连接阶段-准备,属性是如何加载的
public int n1 = 10;
public static int n2 = 20;
public static final int n3 = 30;
}
}
-
代码说明:
-
n1 是实例属性, 不是静态变量,因此在准备阶段,是不会分配内存
-
n2 是静态变量,在该阶段 JVM 会为其分配内存,n2 默认初始化的值为 0 ,而不是 20
-
n3 被 static final 修饰,是常量, 它和静态变量不一样, 其一旦赋值后值就不变,因此其默认初始化 n3 = 30
-
5.4 连接阶段——解析
JVM 将常量池内的符号引用替换为直接引用的过程。
5.5 初始化阶段
- 在初始化阶段,JVM 才会真正执行类中定义的 Java程序代码,此阶段是执行<clinit>() 方法的过程。
- <clinit>() 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有
静态变量
的赋值操作和静态代码块
中的语句,并进行合并的过程。- JVM 会保证一个类的 <clinit>() 方法 在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。
举例如下:
public class ClassLoad {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(B.num);// 直接使用类的静态属性,也会导致类的加载
}
}
class B {
static { // 静态代码块
System.out.println("B 静态代码块被执行");
num = 300;
}
static int num = 100;// 静态变量
public B() {// 构造器
System.out.println("B() 构造器被执行");
}
}
输出如下:
B 静态代码块被执行
100
代码说明:
- 加载阶段:加载 B类,并生成 B的 class对象
- 连接阶段:进行默认初始化 num = 0
- 初始化阶段:执行
<clinit>() 方法
,该方法会依次自动收集类中的所有静态变量
的赋值操作和静态代码块
中的语句,并合并。如下:
clinit() {
System.out.println("B 静态代码块被执行");
num = 300;
num = 100;
}
- 合并后: num = 100
注意:加载类的时候,具有同步机制控制。如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//正因为有这个机制,才能保证某个类在内存中, 只有一份Class对象
synchronized (getClassLoadingLock(name)) {
//....
}
}
二、Class 类
Class
也是一个类,其类名就叫Class
,因此它也继承 Object 类Class
类对象不是由我们程序员创建(new)出来的,而是在类加载时由 JVM 自动创建的- 在
堆内存
中最多只会存在某个类的唯一的Class
对象,因为类只会加载一次 - 每个类的实例对象都会知道自己对应的
Class
对象 - 通过
Class
类对象可以完整地得到其对应的类的信息,通过一系列反射 API - 类的字节码二进制数据,是存放在方法区的,又称为
类的元数据
(包括方法代码、变量名、方法名、访问权限等等)
除了int
等基本类型外,Java的其他类型全部都是class
(包括interface
)。例如:
String
Object
Runnable
Exception
- …
仔细思考,我们可以得出结论:类
class
(包括接口interface
)的本质是数据类型(Type
)。无继承关系的数据类型无法赋值:
Number n = new Double(123.456); // 编译成功
String s = new Double(123.456); // 编译错误
而类
class
是由 JVM 在执行过程中动态加载的。JVM在第一次读取到一种类class
时,会将其加载进内存。
每加载一种
class
,JVM就为其创建一个Class
类的对象,并将两者关联起来。注意:这里的Class
类是一个名字叫Class
的类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
类对象,我们程序员自己的 Java 程序是无法创建Class
类对象的。
所以,JVM持有的每个
Class
类对象都指向一个数据类型(class
或interface
):
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Random
├───────────────────────────┤
│name = "java.util.Random" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘
一个
Class
类对象包含了其对应的类class
的所有完整信息:
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
├───────────────────────────┤
│package = "java.lang" │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,... │
├───────────────────────────┤
│method = indexOf()... │
└───────────────────────────┘
由于JVM为每个加载的类class
创建了对应的Class
类对象,并在实例中保存了该类class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class
类对象,我们就可以通过这个Class
类对象获取到其对应的类class
的所有信息。
这种通过Class
实例获取类class
信息的方法称为反射(Reflection)。
如何获取一个class
的Class
实例?有5个方法:
方法一:直接通过一个类
class
中的静态变量class
获取:
Class cls = String.class;// class 是 String 类中的一个静态变量
方法二:如果我们有一个类
class
的对象,可以通过该对象引用提供的getClass()
方法获取:
String s = "Hello";
Class cls = s.getClass();// 调用 String类对象 s的 getClass() 方法获取
方法三:如果知道一个类
class
的完整类名,可以通过Class
类的静态方法Class.forName()
获取:
Class cls = Class.forName("java.lang.String");// java.lang.String 是 String 类的完整类名
方法四:对于基本数据类型(int、char、boolean、float 等),通过 基本数据类型.class 获取:
Class integerClass = int.class;
Class characterClass = char.class;
Class booleanClass = boolean.class;
System.out.println(integerClass);// int
方法五:对于基本数据类型对应的包装类,可以通过类中的静态变量
TYPE
获取到Class
类对象:
Class type1 = Integer.TYPE;
Class type2 = Character.TYPE;
System.out.println(type1);// int
- 注意:对于基本数据类型获取到的
Class
类对象和基本数据类型对应的包装类获取到的Class
类对象,是同一个Class
类对象:
System.out.println(integerClass.hashCode());
System.out.println(type1.hashCode());// 两者相等,说明都是指向 int
因为
Class
类对象在 JVM 中是唯一的,所以,上述方法获取的Class
类对象是同一个对象。可以用==
比较两个Class
类对象:
Class cls1 = String.class;
String s = "Hello";
Class cls2 = s.getClass();
boolean sameClass = cls1 == cls2; // true
注意一下用
==
比较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
实例。
- 通常情况下,我们应该用
因为反射的目的是为了获得某个类的实例对象的信息。因此,当我们拿到某个
Object
对象时,可以通过反射直接获取该Object
的class
信息,而不需要使用向下转型
:
void printObjectInfo(Object obj) {
Class cls = obj.getClass();
}
要从
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[]
)也是一种类,而且不同于String.class
,它的类名是[Ljava.lang.String;
。此外,JVM为每一种基本类型如int
也创建了Class
实例,通过int.class
访问。
如果获取到了一个
Class
类对象,我们就可以通过该Class
类对象来创建其对应类的实例对象:
// 获取 String 的 Class 类对象:
Class cls = String.class;
// 通过 String 的 Class 类对象创建一个 String 类的实例对象:
String s = (String) cls.newInstance();
- 上述代码相当于
new String()
。通过Class.newInstance()
可以创建类的实例对象,它的局限是:只能调用public
的无参数构造方法。带参数的构造方法,或者非public
的构造方法都无法通过Class.newInstance()
被调用。
1. 动态加载
JVM在执行 Java程序的时候,并不是一次性把所有用到的
class
全部加载到内存,而是第一次需要用到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
类对象Main.class
加载到内存中。然而,并不会加载Person.class
,除非程序执行到create()
方法,JVM 发现需要加载Person
类时,才会首次加载Person
类对应的Class
类对象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 的原因。
2. 小结
- JVM为每个加载的类
class
及接口interface
创建了对应的Class
类对象来保存class
及interface
的所有信息; - 获取一个类
class
对应的Class
类对象后,就可以获取该类class
的所有信息; - 通过 Class类对象获取
class
信息的方法称为反射(Reflection); - JVM 总是动态加载
class
,可以在运行期根据条件来控制加载类class
。
三、访问字段
对任意的一个Object
实例,只要我们获取了它对应的Class
类对象,就可以获取它的一切信息。
我们先看看如何通过
Class
类对象获取其对应的类定义的字段信息。Class
类提供了以下几个方法来获取字段:
Field getField(name)
:根据字段名获取某个 public 的 field(包括父类)
Field getDeclaredField(name)
:根据字段名获取当前类的某个 field(不包括父类)
Field[] getFields()
:获取所有 public 的 field(包括父类)
Field[] getDeclaredFields()
:获取当前类的所有 field(不包括父类)
我们来看一下示例代码:
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}
class Student extends Person {
public int score;
private int grade;
}
class Person {
public String name;
}
- 上述代码首先获取
Student
的Class
实例,然后,分别获取public
字段、继承的public
字段以及private
字段,打印出的Field
类似下面:
public int Student.score
public java.lang.String Person.name
private int Student.grade
-
一个
Field
对象包含了一个字段的所有信息:-
getName()
:返回字段名称,例如,"name"
; -
getType()
:返回字段类型,也是一个Class
类对象,例如,String.class
; -
getModifiers()
:返回字段的修饰符,它是一个int
,不同的 bit 表示不同的含义。
-
以
String
类的value
字段为例,它的定义是:
public final class String {
private final byte[] value;
}
我们用反射获取该字段的信息,代码如下:
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
1. 获取字段值
利用反射拿到字段的一个Field
类对象只是第一步,我们还可以拿到一个实例对象对应的该字段的值。
例如,对于一个
Person
类对象,我们可以先拿到其name
字段对应的Field
,再获取这个Person
类对象的name
字段的 值:
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");// 获取 private String name;
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
-
上述代码先获取
Person
类对应的Class
类对象,再通过该Class
类对象获取Field
类对象,然后,用Field.get(Object)
获取指定Person
类对象的指定字段的值。 -
运行代码,如果不出意外,会得到一个
IllegalAccessException
异常,这是因为name
被定义为一个private
字段,正常情况下,Main
类无法访问Person
类的private
字段。要修复错误,可以将private
改为public
,或者,在调用Object value = f.get(p);
前,先写一句:
f.setAccessible(true);
-
调用
Field.setAccessible(true)
的意思是,别管这个字段是不是public
,一律允许访问。 -
可以试着加上上述语句,再运行代码,就可以打印出
private
字段的值。
有童鞋会问:如果使用反射可以获取private
字段的值,那么类的封装还有什么意义?
-
答案是一般情况下,我们总是通过
p.name
来访问Person
的name
字段,编译器会根据public
、protected
和private
这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。 -
而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)
可能会失败。 如果 JVM 运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java
和javax
开头的package
的类调用setAccessible(true)
,这样可以保证 JVM 核心库的安全。
2. 设置字段值
通过 Field 类对象既然可以获取到指定对象的字段值,自然也可以设置字段的值。
设置字段值是通过
Field.set(Object, Object)
实现的,其中第一个Object
参数是指定的对象,第二个Object
参数是待修改的值。示例代码如下:
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");// 获取 private String name;
f.setAccessible(true);// 允许对 private 字段进行访问
f.set(p, "Xiao Hong");// 设置 p 的 name 的值
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
-
运行上述代码,输出的
name
字段从Xiao Ming
变成了Xiao Hong
,说明通过反射可以直接修改指定对象的字段的值。 -
同样的,修改非
public
字段,需要调用setAccessible(true)
。
3. 小结
- Java 的反射 API 提供的
Field
类封装了对应的类定义的全部字段的所有信息: - 通过
Class
类对象的方法可以获取Field
类对象:getField()
,getFields()
,getDeclaredField()
,getDeclaredFields()
; - 通过
Field
类对象可以获取类定义字段信息:getName()
,getType()
,getModifiers()
; - 通过
Field
类对象可以读取或设置
某个对象的字段的值,如果存在访问限制,则需要调用setAccessible(true)
来访问非public
字段。 - 通过反射读写字段是一种非常规的方法,它会破坏对象的封装。
四、调用方法
我们已经能通过
Class
类的Field
类对象获取其对应的类class
中定义的所有字段信息,同样的,可以通过Class
类获取所有Method
信息。Class
类提供了以下几个方法来获取类class
中定义的Method
:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
我们来看一下示例代码:
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取 public方法 getScore,形参类型为 String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的 public方法 getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取 private方法 getGrade,形参类型为 int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
- 上述代码首先获取
Student
的Class
类对象,然后,分别获取Student
类中定义的public
方法、继承的public
方法以及private
方法,打印出的Method
类似:
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
一个Method
类对象包含一个方法的所有信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法的返回值类型,也是一个Class实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的 bit 表示不同的含义。
1. 调用方法
当我们获取到一个
Method
类对象时,就可以对它进行调用。我们以下面的代码为例:
// 一般情况下调用 String 类的 substring() 方法
String s = "Hello world";
String r = s.substring(6); // "world"
如果用反射来调用
substring
方法,需要以下代码:
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// String 对象:
String s = "Hello world";
// 获取 String substring(int)方法,形参为 int:
Method m = String.class.getMethod("substring", int.class);
// 在 s 对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}
-
注意到
substring()
有两个重载方法,我们获取的是String substring(int)
这个方法(即形参类型为 int,且只有一个)。思考一下如何获取String substring(int, int)
方法。 -
对
Method
类对象调用invoke
方法就相当于调用该substring(int)
方法,invoke
的第一个参数是实例对象(即在哪个实例对象上调用该方法),后面的实参要与方法参数的类型一致,否则将报错。
2. 调用静态方法
如果获取到的
Method
表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke
方法传入的第一个参数永远为null
。我们以Integer.parseInt(String)
方法为例:
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 获取 Integer.parseInt(String) 方法,参数为 String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);// 12345
}
}
3. 调用非 public方法
和
Field
类对象类似,对于非 public 方法,我们虽然可以通过Class.getDeclaredMethod()
获取该方法的实例对象,但直接对其调用将得到一个IllegalAccessException
异常。为了调用非 public 方法,我们通过Method.setAccessible(true)
允许其调用:
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);// Bob
}
}
class Person {
String name;
private void setName(String name) {
this.name = name;
}
}
- 同样,
setAccessible(true)
可能会失败。如果 JVM 运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java
和javax
开头的package
的类调用setAccessible(true)
,这样可以保证 JVM 核心库的安全。
4. 多态
我们来考率这样一种情况:一个
Person
类定义了hello()
方法,并且它的子类Student
也重写了hello()
方法,那么,从Person.class
获取的Method
,作用于Student
类对象时,调用的hello()
方法到底是哪个?
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 获取Person的 hello方法:
Method h = Person.class.getMethod("hello");
// 对 Student实例调用 hello方法:
h.invoke(new Student());
}
}
class Person {
public void hello() {
System.out.println("Person:hello");
}
}
class Student extends Person {
public void hello() {
System.out.println("Student:hello");
}
}
- 运行上述代码,发现输出的是
Student:hello
,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的重写方法(如果存在)。 上述的反射代码:
Method m = Person.class.getMethod("hello");
m.invoke(new Student());
- 实际上相当于:
Person p = new Student();
p.hello();
5. 小结
- Java 的反射 API 提供的
Method
类对象封装了类定义的全部方法的所有信息: - 通过
Class
类对象的方法可以获取Method
类对象:getMethod()
,getMethods()
,getDeclaredMethod()
,getDeclaredMethods()
; - 通过
Method
类对象可以获取方法信息:getName()
,getReturnType()
,getParameterTypes()
,getModifiers()
; - 通过
Method
类对象可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
; - 通过设置
setAccessible(true)
来访问非public
方法; - 通过反射调用方法时,仍然遵循多态原则。
五、调用构造方法
一般情况下,我们通常使用
new
操作符创建新的对象:
Person p = new Person();
如果通过反射来创建新的对象,可以调用
Class
提供的newInstance()
方法:
Person p = Person.class.newInstance();
- 调用
Class.newInstance()
的局限是,它只能调用该类的public
无参构造方法。如果构造方法带有参数,或者不是public
,就无法直接通过Class.newInstance()
来调用。
为了调用任意的构造方法,Java 的反射 API 提供了
Constructor
类对象,它包含一个构造方法的所有信息,通过Constructor
类对象可以创建一个类的实例对象。Constructor
类对象和Method
类对象非常相似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回一个类的实例对象:
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法 Integer(int),形参为 int
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
// 传入的形参必须与构造方法的形参类型相匹配
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String),形参为 String
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
通过Class实例获取Constructor的方法如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
注意:Constructor
类对象只含有当前类定义的构造方法,和父类无关,因此不存在多态的问题。
同样,调用非public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。但setAccessible(true)
也可能会失败。
小结
Constructor
类对象封装了其对应的类定义的构造方法的所有信息;- 通过
Class
类对象可以获取Constructor
类对象:getConstructor()
,getConstructors()
,getDeclaredConstructor()
,getDeclaredConstructors()
; - 通过
Constructor
类对象可以创建一个对应类的实例对象:newInstance(Object... parameters)
; 通过设置setAccessible(true)
来访问非public
构造方法。
六、获取继承方法
当我们获取到某个
Class
类对象时,实际上就获取到了一个类的类型:
Class cls = String.class; // 获取到 String 的 Class类对象
还可以用类对象的
getClass()
方法获取:
String s = "";
Class cls = s.getClass(); // s是String,因此获取到String的Class
最后一种获取
Class
的方法是通过Class.forName("")
,传入Class
的完整类名获取:
Class s = Class.forName("java.lang.String");
这三种方式获取的Class
类对象都是同一个对象,因为 JVM 对每个加载的Class
只创建一个Class
类对象来表示它的类型。
1. 获取父类的Class
有了Class
类对象,我们还可以获取它的父类的Class
类对象:
public class Main {
public static void main(String[] args) throws Exception {
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
}
}
- 运行上述代码,可以看到,
Integer
的父类类型是Number
,Number
的父类是Object
,Object
的父类是null
。除Object
外,其他任何非接口interface
的Class
类对象都必定存在一个父类类型。
2. 获取interface
由于一个类可能实现一个或多个接口,通过
Class
我们就可以查询到实现的接口类型。例如,查询Integer
实现的接口:
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
运行上述代码可知,Integer
实现的接口有:
- java.lang.Comparable
- java.lang.constant.Constable
- java.lang.constant.ConstantDesc
要特别注意:
getInterfaces()
方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:
// reflection
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class.getSuperclass();
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
Integer
的父类是Number
,Number
类实现的接口是java.io.Serializable
。
此外,对所有接口
interface
的Class
类对象调用getSuperclass()
返回的是null
,获取接口的父接口要用getInterfaces()
:
System.out.println(java.io.DataInputStream.class.getSuperclass());
// 输出 java.io.FilterInputStream。因为 DataInputStream 继承自 FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass());
// 输出 null。因为对接口调用 getSuperclass()总是返回 null,获取接口的父接口要用 getInterfaces()
- 如果一个类没有实现任何
interface
,那么getInterfaces()
返回空数组。
3. 继承关系
当我们判断一个对象是否是某个类型时,正常情况下,使用
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 = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
4. 小结
-
通过
Class
对象可以获取继承关系:-
Class getSuperclass()
:获取父类类型; -
Class[] getInterfaces()
:获取当前类实现的所有接口。
-
-
通过
Class
对象的isAssignableFrom()
方法可以判断一个向上转型是否可以实现。
七、动态代理
我们来比较 Java 的类class
和接口interface
的区别:
- 可以实例化类
class
(非abstract
); - 不能实例化接口
interface
。
所有接口
interface
类型的变量总是通过某个实现了接口的类的对象向上转型再赋值给接口类型的变量:
CharSequence cs = new StringBuilder();
有没有可能不编写实现类,直接在运行期创建某个interface
的实例呢?
这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface
的实例。
什么叫运行期动态创建
?听起来好像很复杂。所谓动态代理
,是和静态相对应的。我们来看静态代理代码怎么写:
一、定义接口:
public interface Hello {
void morning(String name);
}
二、编写实现类:
public class HelloWorld implements Hello {
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}
三、创建实例,转型为接口并调用:
Hello hello = new HelloWorld();
hello.morning("Bob");
- 这种方式就是我们通常编写代码的方式。
还有一种方式是动态代码,我们仍然先定义了接口Hello
,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个Proxy.newProxyInstance()
方法创建了一个Hello
接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代理
。JDK 提供的动态创建接口对象的方式,就叫动态代理
。
一个最简单的动态代理实现如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
在运行期动态创建一个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 });
}
}
- 其实就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。
小结
-
Java 标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
-
动态代理是通过
Proxy
创建代理对象,然后将接口方法“代理”给InvocationHandler
完成的。
更多推荐
所有评论(0)