avatar


9.类的加载与反射

IDEA

IDEA代码提示的实现原理,就和我们这一章的主题有关,类的加载与反射。

类的加载与反射联系非常紧密的两部分。可以这么说:类的加载是反射的基础,反射是类的加载的某种应用。

其实我们已经用过类的加载与反射了,在《4.集合》这一章,当时我们讨论了泛型,还讨论了泛型的原理是类型擦除,并且说了有四种类型擦除方法:

  1. 无限制类型擦除
  2. 有限制类型擦除
  3. 擦除方法中类型定义的参数
  4. 桥接方法

然后我们还对这四种类型擦除的方法进行了验证,验证的方法就是类的加载与反射。

此外,Spring、SpringMVC,SpringBoot等等各种框架,这些框架的实现原理也都离不开反射。

加载过程

我们从类的加载过程开始,一步一步讨论到反射。

Java代码三个阶段

在上一章《8.多线程 [2/2]》,我们讨论过Java内存模型的工作。

我们注意这么两个步骤。
1. 首先类加载器将Java代码对应的Class文件加载到方法区。
8. 例如我们的代码,创建了Object对象。那么这个对象同步到堆中。

这两个步骤其实对应着Java代码的两个阶段,再加上我们写的Java代码。则Java代码一共有三个阶段。
如图所示:
Java代码三个阶段

  1. Source源代码阶段:我们写的Person.java,以及编译成字节码文件Person.class,这些都在硬盘中。
  2. Class类对象阶段:类加载器把字节码文件加载进内存,把Person.class字节码文件在内存中表述为java.lang.Class类的对象。
  3. Runtime运行时阶段:创建Person对象。

从第一个阶段到第二个阶段,JVM做了三件事情:

  1. 类的加载
  2. 类的连接
  3. 类的初始化

这就是类的加载过程。

对象的生命周期

此外,我们还可以从对象的生命周期来讨论类的加载过程。
对象的生命周期如图所示:
对象的生命周期

  • 类的加载过程,就是红框框起来的部分。
  • 这张图把类的连接分成了验证、准备和解析三个阶段。

当程序主动使用某个类时,如果该类还未被加载到内存中,则会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这3个步骤,所以有时也把这3个步骤统称为类加载或类初始化。

类的加载

类的加载指的是JVM将类的class文件读入内存,并为之创建java.lang.Class对象,java.lang.Class相当于就是我们类的元数据。

类加载器通常由JVM提供,JVM提供的这些类加载器通常被称为系统类加载器。
当然,我们也可以通过ClassLoader基类来创建自己的类加载器,在本章我们会尝试自己写一个类加载器,帮助我们了解其原理。

我们使用不同的类加载器可以加载不同来源的.class文件,一般情况下我们有几个来源:

  1. 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
  2. 从JAR包加载class文件,这种方式也是很常见的,比如我们使用JDBC时用到的数据库驱动类就放在JAR文件中,JVM 可以从JAR文件中直接加载该class文件。
  3. 通过网络加载class文件。
  4. 把Java源文件动态编译,并执行加载。

类加载器通常无须等到"首次使用"该类时才加载该类,Java虚拟机规范允许系统预先加载某些类

类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE(Java运行时环境)。类连接又可分为如下三个阶段。

  1. 验证
  2. 准备
  3. 解析

验证

验证阶段用于检验被加载的类是否有正确的内部结构,和其他类能不能协调,保证类在运行的时候不会有问题。

准备

类准备阶段则负责为类的类变量分配内存,并设置默认初始值。
例如

1
static int i=5;

首先会为i分配内存,int型则是分配32位的4个字节的内存,然后设置默认值。
默认值是多少?
再回到第一章《1.基础语法》
不同数据类型的默认初始化值不一样,具体如下

  • 整数:默认值 0
  • 浮点数:默认值 0.0
  • 布尔:默认值 false
  • 字符:默认值 空字符
  • 引用数据类型:默认值 null

即,设置默认值为0。

总之,这一步是把内存空间占着。那么?什么时候设置我们写的赋值呢?
这就得看你问的是类还是对象了。如果是类的static值,在初始化的时候。如果是对象,那不在我们这一章的讨论范围内,对象在对象的实例化的时候做。

解析

在类的加载过程中的解析阶段,JVM会把类的二进制数据中的符号引用替换为直接引用。
例如

1
car.run()

在这里,carrun()暂时还只是符号,然后在解析阶段,会被解析成相对应的内存地址的引用(指针)。

现在提一个问题,在JVM内存的虚拟机栈、堆、方法区、程序计数器、本地方法栈,这五个部分的哪个部分?
这个我们在上一章《8.多线程 [2/2]》讨论过,在方法区。

类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量(static变量)进行初始化。
Java类中对类变量指定初始值有两种方式:

  1. 声明类变量时指定初始值。
  2. 使用静态初始化块为类变量指定初始值。

注意!是类的变量!是static变量!

举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class ClassLoaderDemo {
private static int a = 5;
private static int b = 8;
private static int c = 9;
static {
b = 6;
c = 7;
}
public static void main(String[] args) {
System.out.println(a + ", " + b + ", " + c);
}
}

运行结果:

1
5, 6, 7

如果我们改动一下程序:
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class ClassLoaderDemo {
static {
b = 6;
c = 7;
}
private static int a = 5;
private static int b = 8;
private static int c = 9;
public static void main(String[] args) {
System.out.println(a + ", " + b + ", " + c);
}
}

运行结果:

1
5, 8, 9

解释说明:
如果在声明的时候,指定初始值,静态初始化块都将被当成类的初始化语句来执行。这个时候JVM就会从上往下的顺序依次执行。

问题来了

从上往下执行,你先执行了b = 6,但是b的定义的代码是在b = 6的后面啊!
为什么?
因为在初始化阶段之前,还有一个准备阶段。在准备阶段的时候,就把bc定义好了,在内存中占了个位置了。

什么时候初始化

当遇到下面几种情况,就会执行初始化:

  1. 创建类的实例
    为某个类创建实例的方式包括:
    • 使用new操作符来创建实例
    • 通过反射来创建实例
    • 通过反序列化的方式来创建实例)。
  2. 调用某个类的静态方法。
  3. 访问某个类或接口的类变量,或为该类变量赋值。
  4. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
    如代码Class.forName("Person"),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化。并返回Person类对应的java.lang.Class对象。
  5. 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
  6. 直接使用java命令来运行某个主类

那么,什么时候不会初始化呢?
举个反例,这个反例特别的有意思。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class ClassLoaderDemo {
private static class Test {
static {
System.out.println("我是初始化块");
}
public static String text = "kakawanyifan.com";
}
public static void main(String[] args) {
System.out.println(Test.text);
}
}

运行结果:

1
2
我是初始化块
kakawanyifan.com

有什么问题吗?
看起来挺正常的啊。

没问题。
这个不是反例。

我们把public static String text = "kakawanyifan.com"改成public static final String text = "kakawanyifan.com",加一个final

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class ClassLoaderDemo {
private static class Test {
static {
System.out.println("我是初始化块");
}
public static final String text = "kakawanyifan.com";
}
public static void main(String[] args) {
System.out.println(Test.text);
}
}

运行结果:

1
kakawanyifan.com

问题来了吧

只打印了kakawanyifan.com
为什么?

我们通过IDEA反编译class文件之后的内容。
示例代码:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.kakawanyifan;

public class ClassLoaderDemo {
public ClassLoaderDemo() {
}

public static void main(String[] args) {
System.out.println("kakawanyifan.com");
}

private static class Test {
public static final String text = "kakawanyifan.com";

private Test() {
}

static {
System.out.println("我是初始化块");
}
}
}

问题来了吧

我们代码写的是System.out.println(Test.text),但是在这里是System.out.println("kakawanyifan.com")

因为,对于我们final类型的类变量,如果该类变量的值在编译的时候就可以确定下来,Java编译器在编译的时候,就会产生代码优化,编译器会直接把这个类变量出现的地方全部替换成对应的值。

所以呢,Test.text就被替换成了"kakawanyifan.com"
所以呢,System.out.println("kakawanyifan.com")和Test这个类,一点关系都没有。
所以呢,Test类没有被初始化,所以没有打印我是初始化块

加载器

通过上文的讨论,我们已经知道了类加载器的作用。
类加载器负责将class文件加载到内存中,并为之生成对应的java.lang.Class对象。

现在,我们来详细的讨论一下类加载器。

一个类只会被加载一次

类加载器负责加载所有的类,系统为所有被载入内存中的类生成java.lang.Class实例。只要一个类被载入JVM中,同一个类就不会被再次载入了。
现在的问题是,怎么样才算"同一个类"?用包名全路径+类名来标识。

在下文,我们还会用代码来验证,是不是一个类只会被加载一次。

三种类加载器

JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。

  1. Bootstrap ClassLoader: 根类加载器
  2. Platform ClassLoader: 平台类加载器
  3. System ClassLoader: 系统类加载器

类加载器的继承关系:System的父加载器为Platform,而Platform的父加载器为Bootstrap。

  • 注意,我们这里说的是类加载器的继承关系,而不是类的继承关系!
  • 有些资料会说,System的"父类"为Platform,Platform的"父类"为Bootstrap。实际上这种说法欠妥,是"父加载器",不是"父类",他们之间并不是类的继承关系!

我们还可以通过代码简单看看这几个类的加载器。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

public class ClassLoaderDemo {
public static void main(String[] args) {
//首先获取对应的系统加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统加载器:" + systemLoader);

//获取系统加载器的父加载器(平台类加载器)
ClassLoader platformLoader = systemLoader.getParent();
System.out.println("平台类加载器" + platformLoader);

//获取根加载器
ClassLoader bootstrapLoader = platformLoader.getParent();
System.out.println("根加载器是:" + bootstrapLoader);
}
}

运行结果:

1
2
3
系统加载器:sun.misc.Launcher$AppClassLoader@18b4aac 2
平台类加载器sun.misc.Launcher$ExtClassLoader@135fbaa4
根加载器是:null

为什么根加载器是null?

这就解释。

根类加载器

Bootstrap ClassLoader,根类加载器。也有资料称其为引导加载器、原始类加载器,它负责加载Java的核心类,这个加载器是由JVM自己实现的(C/C++)实现,我们没办法直接使用到对应的类。
这个加载器加载的类的路径是jdk安装目录下面对应jre/lib目录下的核心库

那么,为什么是null呢?
因为是由JVM自己实现的(C/C++)实现,并没有集成我们的ClassLoader抽象类,所以我们获取不到,然后就成了null了。

平台类加载器

如果是java8以及之前,这个对应的是扩展加载器。加载的是jre/lib/ext目录下的扩展包。而在java8之后,平台类加载器只是为了向后兼容而保留,而不会加载任何东西了。

系统加载器

加载的是对应的入口程序所在的包。

三种类加载机制

类的加载有三种机制:

  1. 全盘负责
  2. 父类委托
  3. 缓存机制

全盘负责

全盘负责,就是当一个类加载器负责加载某class时,该class所依赖的和引用的其他class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

父类委托

父类委托,是先让parent(父)类加载器试图加载该class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

缓存机制

缓存机制将会保证所有加载过的class会被缓存,当程序中需要使用某个class时,类加载器先从缓存区中搜寻该class,只有当缓存区中不存在该class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。
这就是为什么修改了代码后,必须重新启动JVM程序所做的修改才会生效的原因。

有问题吗?
有问题!
好像有一些插件可以做到不用重启?
是的,但是这些插件的实现原理是底层自己去重启了。

类似的加载中的缓存机制,在Node.js中也有。
可以参考《基于JavaScript的前端开发入门:3.Node.js》的"模块加载机制"的"优先从缓存中加载"。

类加载器工作步骤

在讨论了三种类加载器和三种类加载机制之后,就可以讨论类加载器的工作步骤了。
如图所示
类加载器工作步骤

解释一下:

  1. 需要使用某个类的Class
  2. 比如是我们的系统类加载器
  3. 系统类加载器判断是不是已经加载过了
    加载过了就返回。
  4. 如果没有被加载的话,就判断有没有父加载器。
    1. 没有父加载器的话:
      1. 那么这个加载器肯定是根加载器,那就根加载器自己加载吧。
      2. 失败就抛异常,成功就缓存,缓存之后就返回。
    2. 如果有父加载器的话:
      1. 那么就调用父加载器来加载,如果父加载器还有父加载器,那么继续调用父加载器的父加载器。
      2. 如果父加载器加载成功,就缓存,然后返回。
      3. 如果父加载器加载失败,那就当前的子加载器自己来加载。
        1. 如果子加载器加载成功了,就缓存,返回。
        2. 如果子加载器失败了,就抛异常。

反射

接下来我们讨论反射,而我们的反射都基于了Class这个类实现。

获取Class的对象

前面已经介绍过了,每个类被加载之后,系统就会为该类生成一个对应的Class的对象,那么我们通过Class的对象得到的是什么?是原始的类。
在Java中获取Class对象一般有三种方式:

  1. Class.forName(全类名)方法
    使用Class类的forName(String className)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。
  2. 类名.class属性
    调用某个类的class属性来获取该类对应的Class对象。
  3. 对象名.getClass()
    调用某个对象的getClass()方法。该方法是 java.lang.Object 类中的一个方法,所以所有Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。

我们来举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kakawanyifan;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException {
//使用类的class属性来获取该类对应的Class对象
Class c1 = Student.class;
System.out.println(c1.getName());

Class c2 = Student.class;
System.out.println(c1 == c2);
System.out.println("--------");

//调用对象的getClass()方法,返回该对象所属类对应的Class对象
Student student = new Student();
Class c3 = student.getClass();
System.out.println(c1 == c3);
System.out.println("--------");

//使用Class类中的静态方法forName(String className)
Class c4 = Class.forName("com.kakawanyifan.Student");
System.out.println(c1 == c4);
}
}

运行结果:

1
2
3
4
5
6
class com.kakawanyifan.Student
true
--------
true
--------
true

解释说明:
都相等
这就验证了,一个类只会被加载一次。

接下来,我们先来看怎么获取构造方法,再回过头来看获取Class对象的原理。因为这会和我们的一个注意有关。

获取构造方法并使用

获取

方法名 说明
Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组
Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组
Constructor getConstructor(Class<?>… parameterTypes) 返回单个公共构造方法对象
Constructor getDeclaredConstructor(Class<?>… parameterTypes) 返回单个构造方法对象

示例代码:

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
33
34
package com.kakawanyifan;

import java.lang.reflect.Constructor;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

// Constructor<?>[] getConstructors() 返回一个包含 Constructor对象的数组, Constructor对象反映了由该Class对象表示的类的所有公共构造函数
Constructor<?>[] constructors = c.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}

System.out.println("------");

// Constructor<?>[] getDeclaredConstructors() 返回反映由该 Class对象表示的类声明的所有构造函数的 Constructor对象的数组
Constructor<?>[] declaredConstructors = c.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor);
}

System.out.println("------");

// Constructor<T> getConstructor(Class<?>... parameterTypes) 返回一个 Constructor对象,该对象反映由该 Class对象表示的类的指定公共构造函数
// Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回一个 Constructor对象,该对象反映由此 Class对象表示的类或接口的指定构造函数
// 参数:你要获取的构造方法的参数的个数和数据类型对应的字节码文件对象
Constructor<?> constructor = c.getConstructor(String.class,String.class,String.class);
System.out.println(constructor);
Constructor<?> declaredConstructor = c.getDeclaredConstructor(String.class);
System.out.println(declaredConstructor);
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
public com.kakawanyifan.Test(java.lang.String,java.lang.String,java.lang.String)
public com.kakawanyifan.Test()
------
com.kakawanyifan.Test(java.lang.String,java.lang.String)
private com.kakawanyifan.Test(java.lang.String)
public com.kakawanyifan.Test(java.lang.String,java.lang.String,java.lang.String)
public com.kakawanyifan.Test()
------
public com.kakawanyifan.Test(java.lang.String,java.lang.String,java.lang.String)
private com.kakawanyifan.Test(java.lang.String)

实例化

构造方法已经获取到了,那么接下里就是利用构造方法来创建对象了。
那么。来吧。
示例代码:

1
2
3
4
5
6
7
package com.kakawanyifan;

public class ReflectDemo {
public static void main(String[] args){
Test test = new Test();
}
}

但是!这样就不是反射了!我们要通过反射来创建对象!

方法名 说明
T newInstance(Object…initargs) 根据指定的构造方法创建对象

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

Constructor<?> constructor = c.getConstructor(String.class,String.class,String.class);
Object o = constructor.newInstance("A", "B", "C");
System.out.println(o);

Constructor<?> declaredConstructor = c.getDeclaredConstructor(String.class);
Object o1 = declaredConstructor.newInstance("zifu");
System.out.println(o1);

}
}

运行结果:

1
2
3
4
5
6
7
Test{pri='A', de='B', pub='C'}
Exception in thread "main" java.lang.IllegalAccessException: Class com.kakawanyifan.ReflectDemo can not access a member of class com.kakawanyifan.Test with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
at com.kakawanyifan.ReflectDemo.main(ReflectDemo.java:16)

第一个没问题,第二个报错了?
为什么报错?
私有的构造方法。
解决方法是暴力反射。

暴力反射

暴力反射
public void setAccessible(boolean flag),如果flag的值为true,则取消访问检查。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kakawanyifan;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

Constructor<?> declaredConstructor = c.getDeclaredConstructor(String.class);

// 暴力反射
// public void setAccessible(boolean flag):值为true,取消访问检查
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance("zifu");
System.out.println(o);
}
}

运行结果:

1
Test{pri='zifu', de='null', pub='null'}

获取Class的对象的原理

特别注意!不要滥用反射,比如,我们现在学会了怎么用反射来创建一个对象,所以以后我们再也不new一个对象了。不要这样,用反射,会降低性能。
为什么会降低性能?

这就和获取Class的对象的原理有关了。
前两种获取Class对象的方法,Class.forName(全类名)类名.class,你要先加载那个类,然后在反射得到Class的对象,再用Class的对象去实例化。
那么,第三种方法呢?第三种方法就更不得了,你已经实例化得到了对象,然后你反射得到Class的对象,在用Class的对象再去实例化。
何必呢?

先结婚再离婚

获取成员变量并使用

获取

方法名 说明
Field[] getFields() 返回所有公共成员变量对象的数组
Field[] getDeclaredFields() 返回所有成员变量对象的数组
Field getField(String name) 返回单个公共成员变量对象
Field getDeclaredField(String name) 返回单个成员变量对象

示例代码:

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
33
34
35
36
37
package com.kakawanyifan;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

// Field[] getFields() 返回一个包含 Field对象的数组, Field对象反映由该 Class对象表示的类或接口的所有可访问的公共字段
Field[] fields = c.getFields();
for (Field field : fields) {
System.out.println(field);
}

System.out.println("------");

// Field[] getDeclaredFields() 返回一个 Field对象的数组,反映了由该 Class对象表示的类或接口声明的所有字段
Field[] declaredFields = c.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}

System.out.println("------");

// Field getField(String name) 返回一个 Field对象,该对象反映由该 Class对象表示的类或接口的指定公共成员字段
Field pub = c.getField("pub");
System.out.println(pub);

System.out.println("------");

//Field getDeclaredField(String name) 返回一个 Field对象,该对象反映由该 Class对象表示的类或接口的指定声
Field pri = c.getDeclaredField("pri");
System.out.println(pri);
}
}

运行结果:

1
2
3
4
5
6
7
8
9
public java.lang.String com.kakawanyifan.Test.pub
------
private java.lang.String com.kakawanyifan.Test.pri
java.lang.String com.kakawanyifan.Test.de
public java.lang.String com.kakawanyifan.Test.pub
------
public java.lang.String com.kakawanyifan.Test.pub
------
private java.lang.String com.kakawanyifan.Test.pri

赋值

方法名 说明
void set(Object obj,Object value) 给obj对象的成员变量赋值为value

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.kakawanyifan;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

//获取无参构造方法创建对象
Constructor<?> constructor = c.getConstructor();
Object obj = constructor.newInstance();

// Field提供有关类或接口的单个字段的信息和动态访问
// void set(Object obj, Object value) 将指定的对象参数中由此 Field对象表示的字段设置为指定的新值
// 给obj的成员变量addressField赋值为西安
Field pub = c.getField("pub");

pub.set(obj,"A");
System.out.println(obj);
}
}

运行结果:

1
Test{pri='null', de='null', pub='A'}

获取成员方法并使用

获取

方法名 说明
Method[] getMethods() 返回所有公共成员方法对象的数组,包括继承的
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,不包括继承的
Method getMethod(String name, Class<?>… parameterTypes) 返回单个公共成员方法对象
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回单个成员方法象

示例代码:

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
package com.kakawanyifan;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

// Method[] getMethods() 返回一个包含 方法对象的数组, 方法对象反映由该 Class对象表示的类或接口的所有公共方
Method[] methods = c.getMethods();
for (Method method : methods) {
System.out.println(method);
}

System.out.println("------");

// Method[] getDeclaredMethods() 返回一个包含 方法对象的数组, 方法对象反映由 Class对象表示的类或接口的所有声明方法,包括public,protected,default(package)访问和私有方法,但不包括继承方法
Method[] declaredMethods = c.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public java.lang.String com.kakawanyifan.Test.toString()
public void com.kakawanyifan.Test.setPub(java.lang.String)
public java.lang.String com.kakawanyifan.Test.getDe()
public void com.kakawanyifan.Test.setDe(java.lang.String)
public java.lang.String com.kakawanyifan.Test.getPub()
public java.lang.String com.kakawanyifan.Test.getPri()
public void com.kakawanyifan.Test.setPri(java.lang.String)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
------
public java.lang.String com.kakawanyifan.Test.toString()
public void com.kakawanyifan.Test.setPub(java.lang.String)
public java.lang.String com.kakawanyifan.Test.getDe()
public void com.kakawanyifan.Test.setDe(java.lang.String)
public java.lang.String com.kakawanyifan.Test.getPub()
public java.lang.String com.kakawanyifan.Test.getPri()
public void com.kakawanyifan.Test.setPri(java.lang.String)
private java.lang.String com.kakawanyifan.Test.priFunc()

怎么这么多?
因为包括继承的。
Method[] getMethods(),返回所有公共成员方法对象的数组,包括继承的。
但是Method[] getDeclaredMethods(),返回所有成员方法对象的数组,不包括继承的。

继续,演示一下getMethodgetDeclaredMethod

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;


import java.lang.reflect.Method;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");

// Method getMethod(String name, Class<?>... parameterTypes) 返回一个 方法对象,该对象反映由该 Class对象表示的类或接口的指定公共成员方法
// Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回一个 方法对象,它反映此表示的类或接口的指定声明的方法 Class对象

Method toString = c.getMethod("toString");
System.out.println(toString);

Method priFunc = c.getDeclaredMethod("priFunc");
System.out.println(priFunc);
}
}

运行结果:

1
2
public java.lang.String com.kakawanyifan.Test.toString()
private java.lang.String com.kakawanyifan.Test.priFunc()

调用

方法名 说明
Objectinvoke(Object obj,Object… args) 调用obj对象的成员方法,参数是args,返回值是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
26
package com.kakawanyifan;


import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
//获取Class对象
Class<?> c = Class.forName("com.kakawanyifan.Test");
//获取无参构造方法创建对象
Constructor<?> constructor = c.getConstructor();
Object o = constructor.newInstance();

Method toString = c.getMethod("toString");

// 在类或接口上提供有关单一方法的信息和访问权限
// Object invoke(Object obj, Object... args) 在具有指定参数的指定对象上调用此 方法对象表示的基础方法
// Object:返回值类型
// obj:调用方法的对象
// args:方法需要的参数
Object invoke = toString.invoke(o);
System.out.println(invoke);
}
}

运行结果:

1
Test{pri='null', de='null', pub='null'}

方法参数反射

方法参数反射是JDK8的新特性。

方法名 说明
int getParameterCount() 获取该构造器或方法的形参个数。
Parameter[] getParameters() 获取该构造器或方法的所有形参。

上面第二个方法返回了一个Parameter[]数组,Parameter也是JDK8的新特性,每个Parameter对象代表方法或构造器的一个参数。
Parameter也提供了相关方法

方法名 说明
getModifiers() 获取修饰该形参的修饰符
String getName() 获取形参名
Type getParameterizedType() 获取带泛型形参类型
Class<?> getType() 获取形参类型
boolean isNamePresent() 该方法返回该类的class文件中是否包含了方法的形参名信息
boolean isVarArgs() 用于判断该参数是否为个数可变的形参
  • 解释一下boolean isNamePresent(),使用javac命令编译java源文件时,默认生成的class文件并不包含方法的形参名信息,因此如果调用isNamePresent()方法返回false,那么调用 getName()方法也不能得该参数的形参名。如果希望javac命令编译Java源文件时可以保留形参信息,则需要为该命令指定-parameters选项。

举个例子。

示例代码:

1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

import java.util.List;

public class Test {
public void say(String name, Integer age, List<Double> x) {
System.out.println("My name is " + name + ", My age is " + age);
}
}
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
package com.kakawanyifan;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;

public class ReflectDemo {
public static void main(String[] args) throws NoSuchMethodException {
Class<Test> testClz = Test.class;
Method sayMethod = testClz.getMethod("say", String.class, Integer.class, List.class);
//获取该方法参数的个数
System.out.println("sayMethod方法参数的个数是:" + sayMethod.getParameterCount());
//获取该方法所有的参数信息
Parameter[] parameters = sayMethod.getParameters();
int index = 0;
for (Parameter parameter : parameters) {

index ++;

System.out.println(parameter.isNamePresent());
System.out.println("第" + index + "个参数的信息:");
System.out.println("参数名:" + parameter.getName());
System.out.println("形参类型: " + parameter.getType()) ;
System.out.println("泛型类型: " + parameter.getParameterizedType()) ;
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sayMethod方法参数的个数是:3
false
第1个参数的信息:
参数名:arg0
形参类型: class java.lang.String
泛型类型: class java.lang.String
false
第2个参数的信息:
参数名:arg1
形参类型: class java.lang.Integer
泛型类型: class java.lang.Integer
false
第3个参数的信息:
参数名:arg2
形参类型: interface java.util.List
泛型类型: java.util.List<java.lang.Double>

其他方法

还有很多方法,列举部分。我们就不举例子了。

获取内部类:Class<?>[] getDeclaredClasses(),返回该Class对象对应类里包含的全部内部类。

获取一个类实现了那些接口:Class<?>[] getInterfaces(),返回该Class对象对应类所实现的全部接口。

获取一个类对应的父类的Class:Class<? super T> getSuperclass(),返回该Class对象对应类的超类的Class对象。

获取对应类的修饰符:int getModifiers(),返回此类或接口的所有修饰符。修饰符由publicprotectedprivatefinalstaticabstract等对应的常量组成。返回的整数应使用Modifier工具类的方法来解码,才可以获取真实的修饰符。、
示例代码:

1
2
3
4
5
6
7
public static void main(String[] args) throws NoSuchMethodException {
Method runMethod = User.class.getDeclaredMethod("run");
int modifies = runMethod.getModifiers();
System.out.println(Modifier.isPublic(modifies));//是否是public修饰的
System.out.println(Modifier.isPrivate(modifies));
System.out.println(Modifier.isStatic(modifies));
}

获取类所在的包:Package getPackage(),获取此类的包。

获取类名:String getName(),以字符串形式返回此Class对象所表示的类的名称。

获取类名简称:String getSimpleName(),以字符串形式返回此Class对象所表示的类的简称。

判断该类是否为接口:boolean isInterface(),返回此Class对象是否表示一个接口(使用interface定义)。

判断该类是否为数组:boolean isArray(),返回此Class对象是否表示一个数组类。

越过泛型检查

接下来,我们来看一个神奇的现象,越过泛型检查。
通过反射技术,向一个泛型为Integer的集合中添加一些字符串数据

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kakawanyifan;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class ReflectDemo {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//创建集合
ArrayList<Integer> array = new ArrayList<Integer>();

Class<? extends ArrayList> c = array.getClass();
Method m = c.getMethod("add", Object.class);

m.invoke(array,"hello");
m.invoke(array,"world");
m.invoke(array,"java");

System.out.println(array);
}
}

运行结果:

1
[hello, world, java]

为什么会这样?能不能解释?
能解释,联系我们在《4.集合》讨论的泛型中的类型擦除的无限制类型擦除。

那么,IDEA的代码提示是怎么实现的呢?
通过反射技术,我们就可以得到这个类有哪些成员变量、成员方法、构造方法、方法的参数等等各种信息。那么,就可以通过这些实现IDEA的代码提示了。


至此,我们的《基于Java的后端开发入门》系列笔记的《Java基础篇》就算结束了。

中秋快乐

中秋快乐\text{中秋快乐}



很多资料,在Java基础部分,还会讨论lambda和Stream流,我们这里没有讨论。
lambda和Stream流的使用方式就和我们常见的Java代码风格不太一样,这两个是函数函数式编程思想的体现,是JDK8推出的新特性。
而所谓的函数式编程思想,是忽略面向对象的复杂语法,强调做什么,而不是以什么形式去做。

我个人观点,Java就是用来做项目的,特别是大项目。函数式编程?拿Java当MATLAB用吗?所以,我们不会讨论lambda和Stream流。

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10809
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区