当程序需要在运行时发现对象和类的真实信息时,我们有两条路径可选。第一种,如果在编译时和运行时都完全知晓类型的具体信息,那事情就简单了:先用 instanceof 运算符判断,再强制转换类型即可。但更多时候,我们面对的是第二种情况——编译时根本无法预知对象和类可能属于哪些类型,程序只能依赖运行时信息来探索其真实面貌。这时,就必须请出 反射(Reflection) 这个强大的工具了。
获得 Class 对象
每个类被 JVM 加载后,系统都会为其生成一个对应的 Class 对象。这个 Class 对象就像是通往该类的“钥匙”,通过它,我们就能访问到 JVM 中的这个类。获取这把“钥匙”通常有三种方式:
- 使用
Class.forName(String className) 静态方法:需要传入一个字符串参数,即该类的全限定类名(包含包名)。
- 调用某个类的
.class 属性:例如 String.class。这种方式代码更安全,性能也更好,是大部分场景下的首选。
- 调用对象的
getClass() 方法:这是 java.lang.Object 类中的方法,所有 Java 对象都能调用,返回该对象所属类对应的 Class 对象。
拿到了 Class 对象,就等于掌握了分析这个类的“控制台”,接下来就可以调用其丰富的方法来获取详尽的类信息了。
获取 Class 中的信息
Class 类提供了大量实例方法来获取其对应类的详细信息。
获取 Class 对应类所包含的构造器
- Constructor<T> getConstructor(Class<?>... parameterTypes): 返回指定参数列表的 public 构造器。
- Constructor<?>[] getConstructors(): 返回所有 public 构造器。
- Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes): 返回指定参数列表的构造器(与访问权限无关)。
- Constructor<?>[] getDeclaredConstructors(): 返回所有构造器(与访问权限无关)。
获取 Class 对应类所包含的方法
- Method getMethod(String name, Class<?>... parameterTypes): 返回指定名称和参数列表的 public 方法。
- Method[] getMethods(): 返回所有 public 方法。
- Method getDeclaredMethod(String name, Class<?>... parameterTypes): 返回指定名称和参数列表的方法(与访问权限无关)。
- Method[] getDeclaredMethods(): 返回所有方法(与访问权限无关)。
访问 Class 对应类所包含的成员变量
- Field getField(String name): 返回指定名称的 public 成员变量。
- Field[] getFields(): 返回所有 public 成员变量。
- Field getDeclaredField(String name): 返回指定名称的成员变量(与访问权限无关)。
- Field[] getDeclaredFields(): 返回所有成员变量(与访问权限无关)。
访问 Class 对应类上所包含的 Annotation
- <A extends Annotation> A getAnnotation(Class<A> annotationClass): 获取指定类型的注解,不存在则返回 null。
- <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass): (Java 8+) 获取直接修饰该类的指定类型注解。
- Annotation[] getAnnotations(): 返回类上存在的所有注解。
- Annotation[] getDeclaredAnnotations(): 返回直接修饰该类的所有注解。
- <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass): 获取修饰该类的指定类型的多个注解(支持重复注解)。
- <A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass): 获取直接修饰该类的指定类型的多个注解(支持重复注解)。
访问 Class 对象对应类里的内部类
Class<?>[] getDeclaredClasses(): 返回该类的全部内部类。
访问 Class 对象对应类里的外部类
Class<?> getDeclaringClass(): 返回该类所在的外部类。
访问 Class 对象对应类所实现的接口
Class<?>[] getInterfaces(): 返回该类所实现的全部接口。
访问 Class 对象对应类所继承的父类
Class<? super T> getSuperclass(): 返回该类的超类(父类)的 Class 对象。
获取 Class 对象对应类的修饰符、所在包、类名等基本信息
- int getModifiers(): 返回修饰符常量组合,需用 Modifier 工具类解码。
- Package getPackage(): 获取包信息。
- String getName(): 返回全限定类名字符串。
- String getSimpleName(): 返回类简称。
Class 对象判断该类的类型
- boolean isAnnotation(): 是否是注解类型。
- boolean isAnnotationPresent(Class<? extends Annotation> annotationClass): 是否被指定注解修饰。
- boolean isAnonymousClass(): 是否是匿名类。
- boolean isArray(): 是否是数组类。
- boolean isEnum(): 是否是枚举类。
- boolean isInterface(): 是否是接口。
- boolean isInstance(Object obj): 判断 obj 是否是该 Class 的实例(可替代 instanceof)。
特殊说明
getMethod() 和 getConstructor() 方法需要传入 Class<?>... 参数来区分重载。因为确定一个方法需要“方法名+形参类型列表”共同决定。构造器则只需“形参类型列表”,因为构造器名都相同。
例如,对于以下重载方法:
public void info()
public void info(String str)
public void info(String str, Integer num)
要获取第二个 info 方法,代码如下:
// 获取第二个info方法
// 前一个参数指定方法名,后面的个数可变的Class参数指定形参类型列表
clazz.getMethod("info", String.class)
获取第三个 info 方法:
// 获取第三个info方法
// 前一个参数指定方法名,后面的个数可变的Class参数指定形参类型列表
clazz.getMethod("info", String.class, Integer.class)
下面是一个综合示例,展示了如何使用反射获取类的各种信息:
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {}
@Retention(value = RetentionPolicy.RUNTIME)
@interface Annos {
Anno[] value();
}
// 使用4个注解修饰该类
@SuppressWarnings(value = "unchecked")
@Deprecated
// 使用重复注解修饰该类
@Anno
@Anno
public class ClassTest
{
// 为该类定义一个私有的构造器
private ClassTest()
{
}
// 定义一个有参数的构造器
public ClassTest(String name)
{
System.out.println("执行有参数的构造器");
}
// 定义一个无参数的info方法
public void info()
{
System.out.println("执行无参数的info方法");
}
// 定义一个有参数的info方法
public void info(String str)
{
System.out.println("执行有参数的info方法" + ",其str参数值:" + str);
}
// 定义一个测试用的内部类
class Inner
{
}
public static void main(String[] args)
throws Exception
{
// 下面代码可以获取ClassTest对应的Class
Class<ClassTest> clazz = ClassTest.class;
// 获取该Class对象所对应类的全部构造器
Constructor[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest的全部构造器如下:");
for (var c : ctors)
{
System.out.println(c);
}
// 获取该Class对象所对应类的全部public构造器
Constructor[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest的全部public构造器如下:");
for (var c : publicCtors)
{
System.out.println(c);
}
// 获取该Class对象所对应类的全部public方法
Method[] mtds = clazz.getMethods();
System.out.println("ClassTest的全部public方法如下:");
for (var md : mtds)
{
System.out.println(md);
}
// 获取该Class对象所对应类的指定方法
System.out.println("ClassTest里带一个字符串参数的info()方法为:" + clazz.getMethod("info", String.class));
// 获取该Class对象所对应类的上的全部注解
Annotation[] anns = clazz.getAnnotations();
System.out.println("ClassTest的全部Annotation如下:");
for (var an : anns)
{
System.out.println(an);
}
System.out.println("该Class元素上的@SuppressWarnings注解为:" + Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
System.out.println("该Class元素上的@Anno注解为:" + Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
// 获取该Class对象所对应类的全部内部类
Class<?>[] inners = clazz.getDeclaredClasses();
System.out.println("ClassTest的全部内部类如下:");
for (var c : inners)
{
System.out.println(c);
}
// 使用Class.forName方法加载ClassTest的Inner内部类
Class inClazz = Class.forName("ClassTest$Inner");
// 通过getDeclaringClass()访问该类所在的外部类
System.out.println("inClazz对应类的外部类为:" + inClazz.getDeclaringClass());
System.out.println("ClassTest的包为:" + clazz.getPackage());
System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
}
}
关键点:Class 提供了获取构造器、方法、成员变量、内部类、注解等信息的全套功能。但请注意,@SuppressWarnings 注解的保留策略是 SOURCE(仅源代码级别),因此在运行时通过 Class 对象无法访问到它。
方法参数反射
Java 8 在 java.lang.reflect 包中新增了 Executable 抽象基类,它代表可执行的类成员(方法或构造器),是 Constructor 和 Method 的父类。
Executable 提供了获取注解、判断可变参数、获取修饰符等方法,还有两个核心方法用于获取形参信息:
int getParameterCount(): 获取形参个数。
Parameter[] getParameters(): 获取所有形参。Parameter 是 Java 8 新增的 API,代表一个参数。
Parameter 对象本身也提供了丰富的方法:
getModifiers(): 获取形参修饰符。
String getName(): 获取形参名。
Type getParameterizedType(): 获取带泛型的形参类型。
Class<?> getType(): 获取形参类型。
boolean isNamePresent(): 返回 class 文件是否包含形参名信息。
boolean isVarArgs(): 判断是否为可变参数。
重要提示:默认情况下,javac 编译生成的 class 文件不包含方法形参名信息。因此 isNamePresent() 通常返回 false,getName() 也得不到真实参数名。如果需要保留这些信息,必须在编译时使用 -parameters 选项。
下面是一个获取方法参数信息的例子:
import java.lang.reflect.*;
import java.util.*;
class Test
{
public void replace(String str, List<String> list){}
}
public class MethodParameterTest
{
public static void main(String[] args) throws Exception
{
// 获取Test的类
Class<Test> clazz = Test.class;
// 获取Test类的带两个参数的replace()方法
Method replace = clazz.getMethod("replace",
String.class, List.class);
// 获取指定方法的参数个数
System.out.println("replace方法参数个数:" + replace.getParameterCount());
// 获取replace的所有参数信息
Parameter[] parameters = replace.getParameters();
var index = 1;
// 遍历所有参数
for (var p : parameters)
{
if (p.isNamePresent())
{
System.out.println("---第" + index++ + "个参数信息---");
System.out.println("参数名:" + p.getName());
System.out.println("形参类型:" + p.getType());
System.out.println("泛型类型:" + p.getParameterizedType());
}
}
}
}
要使上述代码能打印出参数名,必须使用 -parameters 选项编译:
javac -parameters -d . MethodParameterTest.java
然后执行程序,可以看到输出包含了具体的参数名和泛型信息:
D:\>java MethodParameterTest
replace方法参数个数:2
---第1个参数信息---
参数名:str
形参类型:class java.lang.String
泛型类型:class java.lang.String
---第2个参数信息---
参数名:list
形参类型:interface java.util.List
泛型类型:java.util.List<java.lang.String>
通过本文的梳理,相信你对 Java 反射的核心机制——特别是如何通过 Class 对象全方位操作类信息——有了更系统的认识。反射是框架设计的基石,深入理解其原理,能让你在阅读源码和进行高级开发时更加得心应手。如果你想进一步探讨反射的高级应用或遇到相关问题,欢迎到 云栈社区 与更多的开发者交流。