java中的反射机制

一、反射概述

1. 什么是反射?

反射(Reflection),Java 中的反射机制是指,Java 程序在运行期间可以获取到一个对象的全部信息。

反射机制一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。

2. 反射机制原理

反射机制允许 Java 程序在运行时调用Reflection API取得任何类的内部信息(比如成员变量、构造器、成员方法等),并能操作类的实例对象的属性以及方法。

在Java 程序中,JVM 加载完一个类后,在堆内存中就会产生该类的一个 Class 对象,一个类在堆内存中最多只会有一个 Class 对象,这个Class 对象包含了该类的完整结构信息,我们通过这个 Class 对象便可以得到该类的完整结构信息

这个 Class 对象就像是一面镜子,我们透过这面镜子可以清楚地看到类的结构信息。因此,我们形象的将获取Class对象的过程称为:反射

==Java 反射机制原理示意图:==

在这里插入图片描述

3. 反射优点和缺点

  1. 优点:可以动态地创建和使用对象,反射机制是 Java 框架的底层核心,其使用灵活,没有反射机制,底层框架就失去支撑。
  2. 缺点:使用反射基本是解释执行,对程序执行速度有影响。

4. 类加载概述

==在深入讲解反射前,先来介绍一下 Java中类的加载与反射机制。==

反射机制是 Java实现动态语言的关键,也就是通过反射实现类的动态加载。

  1. 静态加载:编译时就加载相关的类,如果程序中不存在该类则编译报错,依赖性太强。
  2. 动态加载:运行时加载相关的类,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。

==举个例子:==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 类是否存在。

类加载的时机:

  1. 静态加载
  • 当新创建一个对象时(new),该类会被加载;
  • 当调用类中的静态成员时,该类会被加载;
  • 当子类被加载时,其超类也会被加载;
  1. 动态加载
  • 通过反射的方式,在程序运行时使用到哪个类,该类才会被加载;

==类加载的过程图:==

在这里插入图片描述

5. 类加载各阶段完成的功能

  1. 加载阶段:将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,此过程由类加载器完成。
  2. 连接阶段:又分为验证、准备、解析三个小阶段,此阶段会将类的二进制数据合并到 JRE 中。
  3. 初始化阶段:JVM 负责对类的静态成员进行初始化。

==如下图所示:==

在这里插入图片描述

5.1 加载阶段

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、jar 包、甚至网络文件)转换为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

5.2 连接阶段——验证

在这里插入图片描述

5.3 连接阶段——准备

JVM 会在该阶段对静态变量分配内存并进行默认初始化(不同数据类型会有其默认初始值,如:int —- 0,boolean —- false 等)。这些变量的内存空间会在方法区中分配。

==举例如下:==

1
2
3
4
5
6
7
8
9
10
public class ClassLoad {
public static void main(String[] args) {
// 属性=成员变量=字段
// 类加载的连接阶段-准备,属性是如何加载的

public int n1 = 10;
public static int n2 = 20;
public static final int n3 = 30;
}
}
  • 代码说明:
    1. n1 是实例属性, 不是静态变量,因此在准备阶段,是不会分配内存
    2. n2 是静态变量,在该阶段 JVM 会为其分配内存,n2 默认初始化的值为 0 ,而不是 20
    3. n3 被 static final 修饰,是常量, 它和静态变量不一样, 其一旦赋值后值就不变,因此其默认初始化 n3 = 30

5.4 连接阶段——解析

JVM 将常量池内的符号引用替换为直接引用的过程。

5.5 初始化阶段

  1. 在初始化阶段,JVM 才会真正执行类中定义的 Java程序代码,此阶段是执行() 方法的过程。
  2. () 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值操作和静态代码块中的语句,并进行合并的过程。
  3. JVM 会保证一个类的 () 方法 在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其他线程都要阻塞等待,直到活动线程执行 () 方法完毕。

==举例如下:==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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() 构造器被执行");
}
}

==输出如下:==

1
2
B 静态代码块被执行
100

代码说明:

  1. 加载阶段:加载 B类,并生成 B的 class对象
  2. 连接阶段:进行默认初始化 num = 0
  3. 初始化阶段:执行 <clinit>() 方法,该方法会依次自动收集类中的所有静态变量的赋值操作和静态代码块中的语句,并合并。如下:
1
2
3
4
5
clinit() {
System.out.println("B 静态代码块被执行");
num = 300;
num = 100;
}
  • 合并后: num = 100

注意:加载类的时候,具有同步机制控制。如下:

1
2
3
4
5
6
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//正因为有这个机制,才能保证某个类在内存中, 只有一份Class对象
synchronized (getClassLoadingLock(name)) {
//....
}
}

二、Class 类

Cat 类在「类加载阶段」(JVM 加载 Cat.class 文件时),会自动创建一个唯一的 Class<Cat> 对象(存放在 JVM 的方法区);而每个通过 new Cat() 创建的 Cat 实例对象,都 “知道” 自己对应的这个 Class<Cat> 对象

  • 你写的 class Cat{} 是 “图纸”;
  • JVM 加载图纸后,自动生成 “图纸说明书”(Class<Cat> 对象);
  • 用图纸造出来的 “具体汽车”(new Cat()),每个都带着 ‘说明书索引卡’”—— 所有汽车都指向同一份唯一的说明书(Class<Cat> 对象)(能通过 getClass() 找到说明书)。
  1. Class也是一个类,其类名就叫Class,因此它也继承 Object 类
  2. Class类对象不是由我们程序员创建(new)出来的,而是在类加载时由 JVM 自动创建的
  3. 堆内存中最多只会存在某个类的唯一的Class对象,因为类只会加载一次
  4. 每个类的实例对象都会知道自己对应的Class对象
  5. 通过Class类对象可以完整地得到其对应的类的信息,通过一系列反射 API
  6. 类的字节码二进制数据,是存放在方法区的,又称为类的元数据(包括方法代码、变量名、方法名、访问权限等等)

除了int等基本类型外,Java的其他类型全部都是class(包括interface)。例如:

  • String
  • Object
  • Runnable
  • Exception

仔细思考,我们可以得出结论:类class(包括接口interface)的本质是数据类型(Type)。无继承关系的数据类型无法赋值:

1
2
Number n = new Double(123.456); // 编译成功
String s = new Double(123.456); // 编译错误

而类class是由 JVM 在执行过程中==动态加载==的。JVM在第一次读取到一种类class时,会将其加载进内存。

每加载一种class,JVM就为其创建一个Class类的对象,并将两者关联起来。注意:这里的Class类是一个名字叫Class的类class。它长这样:

1
2
3
public final class Class {
private Class() {}
}

String类为例,当 JVM 加载String类时,它首先读取String.class文件到内存,然后,在堆中为String类创建一个Class类对象并将两者关联起来:

1
Class cls = new Class(String);
  • 注意:这个Class类对象是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现Class类的构造方法是private,即只有 JVM 能创建Class类对象,我们程序员自己的 Java 程序是无法创建Class类对象的。

    • String 类的源码里,没有任何 “创建 Class<String> 对象” 的代码 ——Class<String> 对象的创建,完全是 JVM 的 “自发行为”,和 String 类本身的代码逻辑无关。

      具体创建时机:JVM 加载类时(类加载阶段)

      当 JVM 需要使用某个类(比如 StringCat)时,会执行「类加载流程」,流程的最后一步就是:JVM 自动创建该类对应的 Class 对象,并存放在 JVM 的「方法区」(元空间)。

      String 类为例,创建流程是:

      1. JVM 启动时,会默认加载 java.lang.String 类(因为 String 是核心基础类,必须提前加载);
      2. JVM 读取 String.class 文件(字节码文件),解析里面的元信息(类名、字段、方法等);
      3. JVM 底层调用 Class 类的私有构造方法(private Class()),创建一个 Class<String> 对象;
      4. 把这个 Class<String> 对象存到方法区,后续所有 String 实例(new String("abc"))都通过 “指针” 指向它。

所以,JVM持有的每个Class类对象都指向一个数据类型(classinterface):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Random
├───────────────────────────┤
│name = "java.util.Random" │
└───────────────────────────┘
┌───────────────────────────┐
│ Class Instance │──────> Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘

一个Class类对象包含了其对应的类class的所有完整信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌───────────────────────────┐
│ 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)。

如何获取一个classClass实例?有5个方法:

方法一:直接通过一个类class中的静态变量class获取:

1
Class cls = String.class;// class 是 String 类中的一个静态变量

方法二:如果我们有一个类class的对象,可以通过该对象引用提供的getClass()方法获取:

1
2
String s = "Hello";
Class cls = s.getClass();// 调用 String类对象 s的 getClass() 方法获取

方法三:如果知道一个类class的完整类名,可以通过Class类的静态方法Class.forName()获取:

1
Class cls = Class.forName("java.lang.String");// java.lang.String 是 String 类的完整类名

方法四:对于基本数据类型(int、char、boolean、float 等),通过 基本数据类型.class 获取:

1
2
3
4
5
Class integerClass = int.class;
Class characterClass = char.class;
Class booleanClass = boolean.class;
System.out.println(integerClass);// int

方法五:对于基本数据类型对应的包装类,可以通过类中的静态变量TYPE获取到Class类对象:

1
2
3
Class type1 = Integer.TYPE;
Class type2 = Character.TYPE;
System.out.println(type1);// int
  • 注意:对于基本数据类型获取到的Class类对象和基本数据类型对应的包装类获取到的Class类对象,是同一个Class类对象:
1
2
System.out.println(integerClass.hashCode());
System.out.println(type1.hashCode());// 两者相等,说明都是指向 int

因为Class类对象在 JVM 中是唯一的,所以,上述方法获取的Class类对象是同一个对象。可以用==比较两个Class类对象:

1
2
3
4
5
6
Class cls1 = String.class;

String s = "Hello";
Class cls2 = s.getClass();

boolean sameClass = cls1 == cls2; // true

注意一下用==比较Class类对象和用instanceof的差别:

1
2
3
4
5
6
7
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对象时,可以通过反射直接获取该Objectclass信息,而不需要使用向下转型

1
2
3
void printObjectInfo(Object obj) {
Class cls = obj.getClass();
}

要从Class实例获取获取的基本信息,参考下面的代码(只是简单示范,后面会具体介绍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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类对象来创建其对应类的实例对象:

1
2
3
4
// 获取 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时才加载。例如:

1
2
3
4
5
6
7
8
9
10
11
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 动态加载特性,大致的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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. 小结

  1. JVM为每个加载的类class及接口interface创建了对应的Class类对象来保存classinterface的所有信息;
  2. 获取一个类class对应的Class类对象后,就可以获取该类class的所有信息;
  3. 通过 Class类对象获取class信息的方法称为反射(Reflection);
  4. JVM 总是动态加载class,可以在运行期根据条件来控制加载类class

三、访问字段

对任意的一个Object实例,只要我们获取了它对应的Class类对象,就可以获取它的一切信息。

我们先看看如何通过Class类对象获取其对应的类定义的字段信息。Class类提供了以下几个方法来获取字段:

  1. Field getField(name):根据字段名获取某个 public 的 field(包括父类)
  2. Field getDeclaredField(name):根据字段名获取当前类的某个 field(不包括父类)
  3. Field[] getFields():获取所有 public 的 field(包括父类)
  4. Field[] getDeclaredFields():获取当前类的所有 field(不包括父类)

==我们来看一下示例代码:==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
  • 上述代码首先获取StudentClass实例,然后,分别获取public字段、继承的public字段以及private字段,打印出的Field类似下面:
1
2
3
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字段为例,它的定义是:

1
2
3
4
public final class String {
private final byte[] value;
}

我们用反射获取该字段的信息,代码如下:

1
2
3
4
5
6
7
8
9
10
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字段的 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);前,先写一句:
1
f.setAccessible(true);
  • 调用Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。
  • 可以试着加上上述语句,再运行代码,就可以打印出private字段的值。

有童鞋会问:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?

  • 答案是一般情况下,我们总是通过p.name来访问Personname字段,编译器会根据publicprotectedprivate这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。
  • 而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。

此外,setAccessible(true)可能会失败。 如果 JVM 运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。

2. 设置字段值

通过 Field 类对象既然可以获取到指定对象的字段值,自然也可以设置字段的值。

设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的对象,第二个Object参数是待修改的值。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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. 小结

  1. Java 的反射 API 提供的Field类封装了对应的类定义的全部字段的所有信息:
  2. 通过Class类对象的方法可以获取Field类对象:getField()getFields()getDeclaredField()getDeclaredFields()
  3. 通过Field类对象可以获取类定义字段信息:getName()getType()getModifiers()
  4. 通过Field类对象可以读取或设置某个对象的字段的值,如果存在访问限制,则需要调用setAccessible(true)来访问非public字段。
  5. 通过反射读写字段是一种非常规的方法,它会破坏对象的封装。

四、调用方法

我们已经能通过Class类的Field类对象获取其对应的类class中定义的所有字段信息,同样的,可以通过Class类获取所有Method信息。Class类提供了以下几个方法来获取类class中定义的Method

  1. Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  2. Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  3. Method[] getMethods():获取所有publicMethod(包括父类)
  4. Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

==我们来看一下示例代码:==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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";
}
}
  • 上述代码首先获取StudentClass类对象,然后,分别获取Student类中定义的public方法、继承的public方法以及private方法,打印出的Method类似:
1
2
3
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类对象时,就可以对它进行调用。我们以下面的代码为例:

1
2
3
// 一般情况下调用 String 类的 substring() 方法
String s = "Hello world";
String r = s.substring(6); // "world"

如果用反射来调用substring方法,需要以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)方法为例:

1
2
3
4
5
6
7
8
9
10
11
12
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)允许其调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。

4. 多态

我们来考率这样一种情况:一个Person类定义了hello()方法,并且它的子类Student也重写了hello()方法,那么,从Person.class获取的Method,作用于Student类对象时,调用的hello()方法到底是哪个?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的重写方法(如果存在)。 上述的反射代码:
1
2
Method m = Person.class.getMethod("hello");
m.invoke(new Student());
  • 实际上相当于:
1
2
Person p = new Student();
p.hello();

5. 小结

  1. Java 的反射 API 提供的Method类对象封装了类定义的全部方法的所有信息:
  2. 通过Class类对象的方法可以获取Method类对象:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()
  3. 通过Method类对象可以获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()
  4. 通过Method类对象可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
  5. 通过设置setAccessible(true)来访问非public方法;
  6. 通过反射调用方法时,仍然遵循多态原则。

五、调用构造方法

一般情况下,我们通常使用new操作符创建新的对象:

1
Person p = new Person();

如果通过反射来创建新的对象,可以调用Class提供的newInstance()方法:

1
Person p = Person.class.newInstance();
  • 调用Class.newInstance()的局限是,它只能调用该类的public无参构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。

为了调用任意的构造方法,Java 的反射 API 提供了Constructor类对象,它包含一个构造方法的所有信息,通过Constructor类对象可以创建一个类的实例对象。Constructor类对象和Method类对象非常相似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回一个类的实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的方法如下:

  1. getConstructor(Class...):获取某个publicConstructor
  2. getDeclaredConstructor(Class...):获取某个Constructor
  3. getConstructors():获取所有publicConstructor
  4. getDeclaredConstructors():获取所有Constructor

注意:Constructor类对象只含有当前类定义的构造方法,和父类无关,因此不存在多态的问题。

同样,调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。但setAccessible(true)也可能会失败。

小结

  1. Constructor类对象封装了其对应的类定义的构造方法的所有信息;
  2. 通过Class类对象可以获取Constructor类对象:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()
  3. 通过Constructor类对象可以创建一个对应类的实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。

六、获取继承方法

当我们获取到某个Class类对象时,实际上就获取到了一个类的类型:

1
Class cls = String.class; // 获取到 String 的 Class类对象

还可以用类对象的getClass()方法获取:

1
2
String s = "";
Class cls = s.getClass(); // s是String,因此获取到String的Class

最后一种获取Class的方法是通过Class.forName(""),传入Class的完整类名获取:

1
Class s = Class.forName("java.lang.String");

这三种方式获取的Class类对象都是同一个对象,因为 JVM 对每个加载的Class只创建一个Class类对象来表示它的类型。

1. 获取父类的Class

有了Class类对象,我们还可以获取它的父类的Class类对象:

1
2
3
4
5
6
7
8
9
10
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的父类类型是NumberNumber的父类是ObjectObject的父类是null。除Object外,其他任何非接口interfaceClass类对象都必定存在一个父类类型。

2. 获取interface

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

1
2
3
4
5
6
7
8
9
10
11
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()方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:

1
2
3
4
5
6
7
8
9
10
11
12
// 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的父类是NumberNumber类实现的接口是java.io.Serializable

此外,对所有接口interfaceClass类对象调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces()

1
2
3
4
5
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操作符:

1
2
3
4
5
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()方法:

1
2
3
4
5
6
7
8
// 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. 小结

  1. 通过Class对象可以获取继承关系:
    • Class getSuperclass():获取父类类型;
    • Class[] getInterfaces():获取当前类实现的所有接口。
  2. 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

七、动态代理

我们来比较 Java 的类class和接口interface的区别:

  • 可以实例化类class(非abstract);
  • 不能实例化接口interface

所有接口interface类型的变量总是通过某个实现了接口的类的对象向上转型再赋值给接口类型的变量:

1
CharSequence cs = new StringBuilder();

有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?

这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。

什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。我们来看静态代理代码怎么写:

一、定义接口:

1
2
3
public interface Hello {
void morning(String name);
}

二、编写实现类:

1
2
3
4
5
public class HelloWorld implements Hello {
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}

三、创建实例,转型为接口并调用:

1
2
Hello hello = new HelloWorld();
hello.morning("Bob");
  • 这种方式就是我们通常编写代码的方式。

还有一种方式是动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个Proxy.newProxyInstance()方法创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代理。JDK 提供的动态创建接口对象的方式,就叫动态代理

一个最简单的动态代理实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;

  2. 通过

    1
    Proxy.newProxyInstance()

    创建

    1
    interface

    实例,它需要3个参数:

    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
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 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。

小结

  1. Java 标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
  2. 动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

看不懂再看:

一、先看 “不用动态代理” 和 “用动态代理” 的对比(最直观)

假设你要实现Hello接口的morning方法,做同样的事情(打印 “Good morning, XXX”):

方式 1:不用动态代理(你肯定懂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 写一个接口(规则)
interface Hello {
void morning(String name);
}

// 2. 写一个实现类(具体做事的逻辑)
class HelloImpl implements Hello {
@Override
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}

// 3. 用这个实现类
public class Main {
public static void main(String[] args) {
Hello hello = new HelloImpl(); // 直接new实现类对象
hello.morning("Bob"); // 调用方法,输出结果
}
}

流程:写接口 → 写实现类 → new 对象 → 调用方法。

方式 2:用动态代理(核心是 “不用写实现类”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 写一个接口(和上面一样,规则不变)
interface Hello {
void morning(String name);
}

// 2. 不用写实现类! instead,写一个“逻辑处理器”(告诉JVM该做什么)
class MyHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 这里就是原来HelloImpl里morning方法的逻辑
System.out.println("Good morning, " + args[0]);
return null;
}
}

// 3. 让JVM帮你生成“实现类对象”
public class Main {
public static void main(String[] args) {
// 3.1 准备好逻辑处理器(里面是要做的事)
InvocationHandler handler = new MyHandler();

// 3.2 JVM帮你生成Hello接口的对象(不用new HelloImpl!)
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(),
new Class[] {Hello.class},
handler
);

// 3.3 调用方法(和上面一样)
hello.morning("Bob"); // 输出结果
}
}

流程:写接口 → 写逻辑处理器 → JVM 生成对象 → 调用方法。

核心区别:不用写HelloImpl这个实现类了!JVM 帮你 “偷偷造” 了一个能干活的对象。

二、一步步看动态代理是怎么 “干活” 的(像看动画片)

我们把hello.morning("Bob")这一步拆成 3 帧,看每一步到底发生了什么:

帧 1:你调用hello.morning("Bob")

你以为在调用HelloImplmorning方法,但其实hello不是HelloImpl的对象 —— 是 JVM 给你造的 “临时对象”(代理对象)。

帧 2:代理对象 “转手”

这个代理对象自己啥也不会干,它收到morning调用后,立刻转头喊:“handler,你来处理!” 并把 3 个信息传给handler

  • 我是谁(proxy,不用管);
  • 要做什么(method:就是morning方法);
  • 用什么参数(args:就是"Bob")。

帧 3:handler “干活”

handlerinvoke方法被触发,里面就是你写的逻辑(打印 “Good morning, Bob”),执行完就结束了。

一句话总结这个过程:你找代理对象办事,代理对象自己不办,转手交给 handler 办 ——handler 才是真正干活的。

三、动态代理到底省了什么事?(解决实际问题)

假设你现在有 3 个接口,都要加 “调用前打印日志” 的逻辑:

1
2
3
interface Hello { void morning(String name); }
interface Bye { void evening(String name); }
interface Greet { void noon(String name); }

不用动态代理:你要写 3 个实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HelloImpl implements Hello {
@Override
public void morning(String name) {
System.out.println("日志:调用Hello.morning"); // 重复代码
System.out.println("Good morning, " + name);
}
}
class ByeImpl implements Bye {
@Override
public void evening(String name) {
System.out.println("日志:调用Bye.evening"); // 重复代码
System.out.println("Good evening, " + name);
}
}
class GreetImpl implements Greet {
@Override
public void noon(String name) {
System.out.println("日志:调用Greet.noon"); // 重复代码
System.out.println("Good noon, " + name);
}
}

每个实现类都要写一遍 “打印日志”,重复劳动!

用动态代理:只写 1 个 handler,管所有接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1个handler搞定所有接口的日志逻辑
class LogHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 统一打印日志(不用重复写)
System.out.println("日志:调用" + method.getDeclaringClass().getSimpleName() + "." + method.getName());
// 不同接口的业务逻辑
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
} else if (method.getName().equals("evening")) {
System.out.println("Good evening, " + args[0]);
} else if (method.getName().equals("noon")) {
System.out.println("Good noon, " + args[0]);
}
return null;
}
}

// 用的时候,JVM帮你生成3个接口的对象
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new LogHandler();

Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, handler);
Bye bye = (Bye) Proxy.newProxyInstance(Bye.class.getClassLoader(), new Class[]{Bye.class}, handler);
Greet greet = (Greet) Proxy.newProxyInstance(Greet.class.getClassLoader(), new Class[]{Greet.class}, handler);

hello.morning("Bob"); // 日志+业务逻辑
bye.evening("Alice"); // 日志+业务逻辑
greet.noon("Charlie"); // 日志+业务逻辑
}
}
  • 不用写 3 个实现类,省了大量重复代码;
  • 如果要改日志逻辑(比如加时间戳),只改 1 个 handler 就行,不用改 3 个地方。

四、最后记住 3 个 “不用懂但要会用” 的关键点

  1. 动态代理只能代理 “接口”,不能代理普通类;
  2. 你只需要写InvocationHandler(里面是真正要做的事);
  3. Proxy.newProxyInstance就能拿到接口对象,直接调用方法就行。