Java学习笔记(五):反射与注解
声明:本篇笔记部分摘自《Java核心技术(卷Ⅰ) - 机械工业出版社》及Java教程-廖雪峰-2025-06-16,参考了哔哩哔哩上“黑马Java磊哥”的反射与注解专题讲解视频,遵循CC BY 4.0协议。
存在由AI生成的小部分内容,仅供参考,请仔细甄别可能存在的错误。
🤔 反射和注解是Java中的高级技术,常用于开发框架等底层开发工作,应用级开发中的使用频率不高。如果刚入门Java,不是很能理解的话建议先跳过本节😕,等到对Java及面向对象有一定的理解后再来尝试深入学习也不迟。
一、反射
1.引入
考虑这样一个情况,用户成功登录后,后端需要返回用户的昵称、头像、个性签名等数据;常用的方式是将用户对象 User 中的信息的序列化为JSON文件进行传输。
我们可以在这个类中定义一个 ToJson() 方法,将类的一些字段打包整理成规范的格式;但是这样做有很多缺点:
- 如果有很多个类,每个类都需要写一套这样的格式化方法,工作量很大而且重复度很高;
- 如果类中的字段有改动,方法也需要跟着修改,否则会出错误;
- 格式化方法与每一个类深度绑定(相当于是“写死”的),无法预知和处理将来出现的新类。
再来考虑这样一个情况:统计字符串中各个字符出现的次数,我们只需读取整个字符串中的内容,逐个统计其中的每个字符。
回到之前的场景,能否也像这样接收一个类的信息,动态地分析每一个字段并且将他们拼接成JSON字符串呢?类都是定义好的结构和内容,而在程序运行时“查看”类的结构,就是在查看程序本身的一部分结构了。
Java中,在程序运行时“反向”地查看和操作它自身的结构和行为,就是“反射”。
2.Class类
① 什么是Class类
除了int等基本类型外,Java的其他类型全部都是class(包括interface)。例如:
StringObjectRunnableException
我们可以认为类的本质是一种数据类型。JVM在第一次读取到一个类时,将其加载进内存,同时就为其创建一个Class类型的实例(这个实例只能由JVM创建),并关联起来:
1 | |
所以,JVM持有的每个Class实例都指向一个数据类型(或者说是一个类)。Class实例中包含了这个类的所有完整信息:
1 | |
由于JVM为每个加载的类都创建了对应的Class实例,并在实例中保存了这个类的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的类的所有信息。
这种通过Class实例获取类的信息的方法称为反射(Reflection)。
② 获取一个类的Class实例
方法一:直接通过一个class的静态变量class获取:
1 | |
方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:
1 | |
方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:
1 | |
因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。
要从Class实例获取类的基本信息,参考下面的代码:
1 | |
运行上述程序,输出以下内容:
1 | |
注意到数组(例如String[])也是一种类,而且不同于String.class,它的类名是[Ljava.lang.String;。此外,JVM为每一种基本类型如int也创建了Class实例,通过int.class访问。
③ 动态加载
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。即程序运行时,发现需要使用哪一个类,再将其动态添加到内存。
2.访问字段
① 获取所有的字段
Class类提供了以下几个方法来获取字段:
| 方法 | 返回值 |
|---|---|
Field getField(name) |
指定的public字段(包括父类) |
Field getDeclaredField(name) |
当前类的某个指定字段(不包括父类) |
Field[] getFields() |
所有public的字段(包括父类) |
Field[] getDeclaredFields() |
当前类的所有字段(不包括父类) |
这些方法会获取到类似于 private String Person.name 这样的字段信息,包含了可见性、类型、类名.字段名这些信息。 |
获取到字段信息后,可以使用这些方截取取字段的部分信息:
| 方法 | 返回值 |
|---|---|
getName() |
字段名称,如"name" |
getType() |
字段类型,也是一个Class实例 |
getModifiers() |
字段修饰符,是一个int,含义见下表 |
| 修饰符 | 对应的int类型 |
|---|---|
| public | 1 |
| private | 2 |
| protected | 4 |
| static | 8 |
| final | 16 |
| synchronized | 32 |
| volatile | 64 |
| transient | 128 |
| native | 256 |
| interface | 512 |
| abstract | 1024 |
| strict | 2048 |
1 | |
② 获取字段的值
获取到字段后,我们还需要一个方法来获取这些的值:
1 | |
如果有需要的话,我们也可以添加以下语句来访问private字段:
1 | |
反射是一种非常规的用法,反射的代码非常繁琐,其次它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。
③ 设置字段的值
1 | |
3.调用方法
① 获取所有的方法
既然能获取,甚至是设置对象的字段,那么我们也可以获取Class的所有方法信息。以下是几个实现的方法:
| 方法 | 返回值 |
|---|---|
Method getMethod(name, Class...) |
指定的public方法(包括父类) |
Method getDeclaredMethod(name, Class...) |
指定的方法(不包括父类) |
Method[] getMethods() |
所有public的方法(包括父类) |
Method[] getDeclaredMethods() |
所有Method(不包括父类) |
每个方法对象都包含了这个方法的所有信息:
getName():返回方法名称,例如:"getScore";getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
② 调用获取到的方法
1 | |
同样地,我们可以通过 Method.setAccessible(true) 允许调用对象中的非public方法。
4.调用构造方法
如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:
1 | |
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。处理Constructor对象和获取对象的方法很类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:
1 | |
通过Class实例获取Constructor的方法如下:
| 方法 | 返回值 |
|---|---|
getConstructor(Class...) |
获取某个public的Constructor |
getDeclaredConstructor(Class...) |
获取某个Constructor |
getConstructors() |
获取所有public的Constructor |
getDeclaredConstructors() |
获取所有Constructor |
注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。
5.获取继承关系
① 获取父类的Class
获取到Class实例后,我们还可以调用Class实例的方法getSuperclass()获取父类的Class:
1 | |
运行上述代码,可以看到,Integer的父类类型是Number,Number的父类是Object,Object的父类是null。除Object外,其他任何非interface的Class都必定存在一个父类类型。
② 获取接口
由于一个类可能实现一个或多个接口,通过Class的getInterfaces()方法,我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:
1 | |
运行上述代码可知,Integer实现的接口有:
- java.lang.Comparable
- java.lang.constant.Constable
- java.lang.constant.ConstantDesc
要特别注意:getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。如果一个类没有实现任何interface,那么getInterfaces()返回空数组。
③ 继承关系
像在普通对象中调用instanceof()查看对象能否向上转型,我们对Class实例使用isAssignableForm()来查看它们之间的继承关系:
1 | |
6.动态代理
Java的class和interface有这样的区别:
- 可以实例化非抽象的
class; - 不能实例化
interface。
所有interface类型的变量总是通过某个实例向上转型并赋值给接口类型变量的:
1 | |
我们也可以不编写类,直接在运行期间创建某个interface的实例,这就需要使用动态代理(dynamic proxy)的机制。
这一部分先省略吧,毕竟我前面的都不是很懂…实际上反射在应用开发中使用得不多(常用于底层组件开发),我认为应该先掌握更加基础的知识再逐渐深入。
二、注解
注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”:
1 | |
与注释不同的是,注解可以被编译器打包进入.class文件而不是直接被忽略,因此注解是一种用作标注的“元数据”。
1.注解的作用
注解(Annotation)本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。Java注解有以下三种类型:
- 由编译器使用的注解,它们不会被编译进入
.class文件,它们在编译后就被编译器扔掉了。,如:@Override:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings:告诉编译器忽略此处代码产生的警告。
- 由工具处理
.class文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。 - 在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。
2.注解参数
定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String、Class以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外,大部分注解会有一个名为value的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。
1 | |
如果只写注解,相当于全部使用默认值。
3.定义注解
Java语言使用@interface语法来定义注解,它的格式如下:
1 | |
注解的参数类似无参数方法,最好用default设定一个默认值。最常用的参数应当命名为value。
(先忽略掉吧,太抽象了😥等我研究明白再接着往下写,先去学泛型吧……)
参考资料
- 廖雪峰的官方网站.Java教程[EB/OL].(2025-06-07)[2025-08-21]. https://liaoxuefeng.com/books/java/introduction/index.html ↩
- 黑马Java磊哥【黑马磊哥】Java反射、注解、反射机制、反射专题、注解专题、挑战100个Java知识点,相信听完这套课,肯定可以解锁Java反射和注解[EB/OL].(2022-12-01)[2025-08-26]. https://www.bilibili.com/video/BV1DG4y1G7xy/ ↩