Java Reflection使用指南-Java快速入门教程
1. 概述
在本教程中,将探索 Java 反射,它允许检索类、接口、字段和方法的运行时属性。当在编译时不知道它们的名字时,这尤其有用。
此外,可以使用反射实例化新对象、调用方法以及获取或设置字段值。
2. 项目设置
要使用Java反射,不需要包含任何特殊的jar,任何特殊的配置或Maven依赖项。JDK 附带了一组类,这些类专门捆绑在 java.lang.reflect 包中,专门用于此目的。
因此,需要做的就是将以下内容导入到的代码中:
import java.lang.reflect.*;
注:
为了访问实例的类、方法和字段信息,可以调用 getClass 方法,该方法返回对象的运行时类表示形式。返回的类对象提供用于访问有关类的信息的方法。
3. 简单示例
创建一个简单的 Person 类,其中只有名称和年龄字段,没有声明任何方法。
下面是 Person 类:
public class Person {
private String name;
private int age;
}
现在,将使用 Java 反射来发现此类的所有字段的名称。
为了理解反射的力量,让构造一个 Person 对象并使用 Object 作为引用类型:
@Test
public void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() {
Object person = new Person();
Field[] fields = person.getClass().getDeclaredFields();
List<String> actualFieldNames = getFieldNames(fields);
assertTrue(Arrays.asList("name", "age")
.containsAll(actualFieldNames));
}
这个测试向表明,能够从的 person 对象中获取一个 Field 对象的数组,即使对该对象的引用是该对象的父类型。
在上面的示例中,只对这些字段的名称感兴趣。但是还有很多事情可以做,将在下一节中看到这方面的示例。
请注意如何使用帮助程序方法来提取实际的字段名称。
这是一个非常基本的代码:
private static List<String> getFieldNames(Field[] fields) {
List<String> fieldNames = new ArrayList<>();
for (Field field : fields)
fieldNames.add(field.getName());
return fieldNames;
}
4. Java 反射用例
在继续讨论 Java 反射的不同功能之前,将讨论可能发现的一些常见用途。Java反射非常强大,可以通过多种方式非常方便使用。
例如,在许多情况下,有数据库表的命名约定。可以选择通过在表名前面加上tbl_来增加一致性,如将包含订单数据的表称为tbl_order_data。
在这种情况下,可以将保存订单数据的 Java 对象命名为 Order或 OrderData。然后使用 CRUD 范例,为每个操作提供一个入口点,以便创建操作仅接收 Object 参数。
然后,使用反射来检索对象名称和字段名称。此时,可以将此数据映射到数据库表,并将对象字段值分配给适当的数据库字段名称。
5. 检索 Java 类
在本节中,将探讨 Java Reflection API 中最基本的组件。正如前面提到的,Java 类对象使能够访问任何对象的内部详细信息,检索内部细节。例如对象的类名、修饰符、字段、方法、实现的接口等。
5.1. 准备
为了牢牢掌握应用于 Java 类的反射 API 并拥有各种示例,我们先创建一个实现 Eating 接口的抽象 Animal 类。这个接口定义了创建的任何具体动物对象的进食行为契约。
public interface Eating {
String eats();
}
以下是 Eating 接口的具体动物实现:
public abstract class Animal implements Eating {
public static String CATEGORY = "domestic";
private String name;
protected abstract String getSound();
// constructor, standard getters and setters omitted
}
还创建另一个名为 Locomotion 的接口,用于描述动物的移动方式:
public interface Locomotion {
String getLocomotion();
}
现在将创建一个名为 Goat 的具体类,该类扩展了 Animal 并实现了 Locomotion。
由于超类实现了 Eating,Goat 也必须实现该接口的方法:
public class Goat extends Animal implements Locomotion {
@Override
protected String getSound() {
return "bleat";
}
@Override
public String getLocomotion() {
return "walks";
}
@Override
public String eats() {
return "grass";
}
// constructor omitted
}
从现在开始,将使用 Java 反射来检索出现在上述类和接口中的 Java 对象的各个方面。
5.2. 类名
先从 Class 中获取对象的名称开始:
@Test
public void givenObject_whenGetsClassName_thenCorrect() {
Object goat = new Goat("goat");
Class<?> clazz = goat.getClass();
assertEquals("Goat", clazz.getSimpleName());
assertEquals("com.jack.yang.reflection.Goat", clazz.getName());
assertEquals("com.jack.yang.reflection.Goat", clazz.getCanonicalName());
}
请注意,类的 getSimpleName 方法返回对象的基本名称,因为它将出现在其声明中。然后,其他两个方法返回完全限定的类名,包括包声明。
还看看,如果只知道它的完全限定类名,如何创建 Goat 类的对象:
@Test
public void givenClassName_whenCreatesObject_thenCorrect(){
Class<?> clazz = Class.forName("com.jack.yang.reflection.Goat");
assertEquals("Goat", clazz.getSimpleName());
assertEquals("com.jack.yang.reflection.Goat", clazz.getName());
assertEquals("com.jack.yang.reflection.Goat", clazz.getCanonicalName());
}
请注意,传递给静态 forName 方法的名称应包含包信息。否则,将得到一个 ClassNotFoundException。
5.3. 类修饰符
可以通过调用返回 Integer 的 getModifiers 方法来确定类中使用的修饰符。每个修饰符都是一个设置或清除的标志位。
java.lang.reflect.Modifier 类提供了静态方法,用于分析返回的 Integer 是否存在特定修饰符。
让确认上面定义的一些类的修饰符:
@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
Class<?> goatClass = Class.forName("com.jack.yang.reflection.Goat");
Class<?> animalClass = Class.forName("com.jack.yang.reflection.Animal");
int goatMods = goatClass.getModifiers();
int animalMods = animalClass.getModifiers();
assertTrue(Modifier.isPublic(goatMods));
assertTrue(Modifier.isAbstract(animalMods));
assertTrue(Modifier.isPublic(animalMods));
}
5.4. 软件包信息
通过使用Java反射,还能够获取有关任何类或对象的包的信息。此数据捆绑在 Package 类中,该类由对类对象上的 getPackage 方法的调用返回。
让运行一个测试来检索包名称:
@Test
public void givenClass_whenGetsPackageInfo_thenCorrect() {
Goat goat = new Goat("goat");
Class<?> goatClass = goat.getClass();
Package pkg = goatClass.getPackage();
assertEquals("com.jack.yang.reflection", pkg.getName());
}
5.5. 超类
还能够通过使用 Java 反射来获取任何 Java 类的超类。
在许多情况下,特别是在使用库类或Java的内置类时,可能事先不知道正在使用的对象的超类。本小节将介绍如何获取此信息。
让继续确定山羊的超类。
此外,还展示了java.lang.String类是java.lang.Object class的一个子类:
@Test
public void givenClass_whenGetsSuperClass_thenCorrect() {
Goat goat = new Goat("goat");
String str = "any string";
Class<?> goatClass = goat.getClass();
Class<?> goatSuperClass = goatClass.getSuperclass();
assertEquals("Animal", goatSuperClass.getSimpleName());
assertEquals("Object", str.getClass().getSuperclass().getSimpleName());
}
5.6. 实现的接口
使用 Java 反射,还能够获取给定类实现的接口列表。
让检索由 Goat 类和 Animal 抽象类实现的接口的类类型:
@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
Class<?> goatClass = Class.forName("com.jack.yang.reflection.Goat");
Class<?> animalClass = Class.forName("com.jack.yang.reflection.Animal");
Class<?>[] goatInterfaces = goatClass.getInterfaces();
Class<?>[] animalInterfaces = animalClass.getInterfaces();
assertEquals(1, goatInterfaces.length);
assertEquals(1, animalInterfaces.length);
assertEquals("Locomotion", goatInterfaces[0].getSimpleName());
assertEquals("Eating", animalInterfaces[0].getSimpleName());
}
请注意,从断言中可以看出,每个类只实现一个接口。检索这些接口的名称,发现 Goat 实现了 Locomotion,Animal 实现了 Eating,就像它出现在的代码中一样。
可以看到 Goat 是抽象类 Animal 的一个子类,实现了接口方法 eats()。然后,Goat 还实现了 Eating 接口。
因此,值得注意的是,只有那些类显式声明为 implements 关键字实现的接口才会出现在返回的数组中。
因此,即使一个类实现了接口方法,因为它的超类实现了该接口,但子类没有直接声明该接口 implements 关键字,该接口也不会出现在接口数组中。
5.7. 构造函数、方法和字段
通过Java反射,能够检索任何对象的类的构造函数以及方法和字段。
稍后,将能够看到对类的每个组件的更深入检索。但就目前而言,只需获得他们的名字并将其与期望的进行比较就足够了。
让看看如何获取 Goat 类的构造函数:
@Test
public void givenClass_whenGetsConstructor_thenCorrect(){
Class<?> goatClass = Class.forName("com.jack.yang.reflection.Goat");
Constructor<?>[] constructors = goatClass.getConstructors();
assertEquals(1, constructors.length);
assertEquals("com.jack.yang.reflection.Goat", constructors[0].getName());
}
还可以检索 Animal 类的字段:
@Test
public void givenClass_whenGetsFields_thenCorrect(){
Class<?> animalClass = Class.forName("com.jack.yang.reflection.Animal");
Field[] fields = animalClass.getDeclaredFields();
List<String> actualFields = getFieldNames(fields);
assertEquals(2, actualFields.size());
assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
}
同样可以检索 Animal 类的方法:
@Test
public void givenClass_whenGetsMethods_thenCorrect(){
Class<?> animalClass = Class.forName("com.jack.yang.reflection.Animal");
Method[] methods = animalClass.getDeclaredMethods();
List<String> actualMethods = getMethodNames(methods);
assertEquals(4, actualMethods.size());
assertTrue(actualMethods.containsAll(Arrays.asList("getName",
"setName", "getSound")));
}
就像getFieldNames一样,添加了一个帮助程序方法来从Method 对象数组中检索方法名称:
private static List<String> getMethodNames(Method[] methods) {
List<String> methodNames = new ArrayList<>();
for (Method method : methods)
methodNames.add(method.getName());
return methodNames;
}
6. 检索构造函数
通过Java反射,可以检索任何类的构造函数,甚至可以在运行时创建类对象。这是通过java.lang.reflect.Constructor类实现的。
之前,只研究了如何获取构造函数对象的数组,从中可以获取构造函数的名称。
在本节中,将重点介绍如何检索特定的构造函数。
众所周知,在 Java 中,没有一个类的两个构造函数共享完全相同的方法签名。因此,将使用这种唯一性从多个构造函数中获取一个构造函数。
为了理解这个类的功能,将创建一个具有三个构造函数的 Animal 的 Bird 子类。
不会实现 Locomotion,以便可以使用构造函数参数指定该行为,以添加更多种类:
public class Bird extends Animal {
private boolean walks;
public Bird() {
super("bird");
}
public Bird(String name, boolean walks) {
super(name);
setWalks(walks);
}
public Bird(String name) {
super(name);
}
public boolean walks() {
return walks;
}
// standard setters and overridden methods
}
让通过使用反射来确认此类有三个构造函数:
@Test
public void givenClass_whenGetsAllConstructors_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Constructor<?>[] constructors = birdClass.getConstructors();
assertEquals(3, constructors.length);
}
接下来,将通过按声明顺序传递构造函数的参数类类型来检索 Bird 类的每个构造函数:
@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Constructor<?> cons1 = birdClass.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
Constructor<?> cons3 = birdClass.getConstructor(String.class, boolean.class);
}
不需要断言,因为将得到一个 NoSuchMethodException,并且当给定顺序中给定参数类型的构造函数不存在时,测试将自动失败。
在上一个测试中,将了解如何在运行时实例化对象,同时提供其参数:
@Test
public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Constructor<?> cons1 = birdClass.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
Constructor<?> cons3 = birdClass.getConstructor(String.class,
boolean.class);
Bird bird1 = (Bird) cons1.newInstance();
Bird bird2 = (Bird) cons2.newInstance("Weaver bird");
Bird bird3 = (Bird) cons3.newInstance("dove", true);
assertEquals("bird", bird1.getName());
assertEquals("Weaver bird", bird2.getName());
assertEquals("dove", bird3.getName());
assertFalse(bird1.walks());
assertTrue(bird3.walks());
}
通过调用构造函数类的 newInstance 方法并以声明的顺序传递所需的参数来实例化类对象。然后,将结果转换为所需的类型。
也可以使用 Class.newInstance() 方法调用默认构造函数。但是,此方法自Java 9以来已被弃用,不应该在现代Java项目中使用它。
对于 bird1,使用默认构造函数,该构造函数会自动将 Bird 代码中的名称设置为 bird,并通过测试进行确认。
然后,只用一个名字实例化 bird2 并进行测试。请记住,当不设置运动行为时,它默认为 false,如最后两个断言所示。
7. 在运行时检索
以前,只检索字段的名称。在本节中,将展示如何在运行时获取和设置它们的值。
有两种主要方法用于在运行时检索类的字段:getFields() 和 getField(fieldName)。
getFields() 方法返回相关类的所有可访问的公共字段。它将返回类和所有超类中的所有公共字段。
例如,当在 Bird 类上调用此方法时,只会获得其超类 Animal 的 CATEGORY 字段,因为 Bird 本身没有声明任何公共字段:
@Test
public void givenClass_whenGetsPublicFields_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Field[] fields = birdClass.getFields();
assertEquals(1, fields.length);
assertEquals("CATEGORY", fields[0].getName());
}
此方法还有一个名为 getField 的变体,它通过采用字段名称仅返回一个 Field 对象:
@Test
public void givenClass_whenGetsPublicFieldByName_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Field field = birdClass.getField("CATEGORY");
assertEquals("CATEGORY", field.getName());
}
无法访问在超类中声明但未在子类中声明的私有字段。这就是无法访问名称字段的原因。
但是,可以通过调用 getDeclaredFields 方法来检索正在处理的类中声明的私有字段:
@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect(){
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Field[] fields = birdClass.getDeclaredFields();
assertEquals(1, fields.length);
assertEquals("walks", fields[0].getName());
}
如果知道字段的名称,也可以使用它的另一个变体:
@Test
public void givenClass_whenGetsFieldsByName_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Field field = birdClass.getDeclaredField("walks");
assertEquals("walks", field.getName());
}
如果弄错了字段的名称或输入了一个不存在的字段,将得到一个 NoSuchFieldException。
现在将获得字段类型:
@Test
public void givenClassField_whenGetsType_thenCorrect() {
Field field = Class.forName("com.jack.yang.reflection.Bird")
.getDeclaredField("walks");
Class<?> fieldClass = field.getType();
assertEquals("boolean", fieldClass.getSimpleName());
}
接下来,让看看如何访问字段值并修改它们。
要获取字段的值,更不用说设置它了,必须首先通过在 Field 对象上调用 setAccessible 方法来设置它是可访问的,并将布尔值传递给它:
@Test
public void givenClassField_whenSetsAndGetsValue_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Bird bird = (Bird) birdClass.getConstructor().newInstance();
Field field = birdClass.getDeclaredField("walks");
field.setAccessible(true);
assertFalse(field.getBoolean(bird));
assertFalse(bird.walks());
field.set(bird, true);
assertTrue(field.getBoolean(bird));
assertTrue(bird.walks());
}
在上面的测试中,在将 walks 字段的值设置为 true 之前确定它确实是假的。
请注意,如何使用 Field 对象来设置和获取值,方法是将正在处理的类的实例以及可能希望字段在该对象中具有的新值传递给它。
关于 Field 对象需要注意的一件重要事情是,当它被声明为公共静态时,不需要包含它们的类的实例。
可以在其位置传递 null,并且仍然可以获取字段的默认值:
@Test
public void givenClassField_whenGetsAndSetsWithNull_thenCorrect(){
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Field field = birdClass.getField("CATEGORY");
field.setAccessible(true);
assertEquals("domestic", field.get(null));
}
8. 检索方法
在前面的示例中,仅使用反射来检索方法名称。然而,Java反射比这更强大。
通过 Java 反射,可以在运行时调用方法并传递给它们所需的参数,就像对构造函数所做的那样。同样,也可以通过指定每个方法的参数类型来调用重载方法。
就像字段一样,有两种主要方法来检索类方法。getMethods 方法返回类和超类的所有公共方法的数组。
这意味着使用此方法,可以获取java.lang.Object类的公共方法,例如toString,hashCode和notifyAll:
@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Method[] methods = birdClass.getMethods();
List<String> methodNames = getMethodNames(methods);
assertTrue(methodNames.containsAll(Arrays
.asList("equals", "notifyAll", "hashCode",
"walks", "eats", "toString")));
}
要只获取感兴趣的类的公共方法,必须使用 getDeclaredMethods 方法:
@Test
public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect(){
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
List<String> actualMethodNames
= getMethodNames(birdClass.getDeclaredMethods());
List<String> expectedMethodNames = Arrays
.asList("setWalks", "walks", "getSound", "eats");
assertEquals(expectedMethodNames.size(), actualMethodNames.size());
assertTrue(expectedMethodNames.containsAll(actualMethodNames));
assertTrue(actualMethodNames.containsAll(expectedMethodNames));
}
这些方法中的每一个都有单一的变体,它返回一个知道其名称的 Method 对象:
@Test
public void givenMethodName_whenGetsMethod_thenCorrect() throws Exception {
Bird bird = new Bird();
Method walksMethod = bird.getClass().getDeclaredMethod("walks");
Method setWalksMethod = bird.getClass().getDeclaredMethod("setWalks", boolean.class);
assertTrue(walksMethod.canAccess(bird));
assertTrue(setWalksMethod.canAccess(bird));
}
请注意如何检索各个方法并指定它们采用的参数类型。那些不采用参数类型的参数使用空变量参数进行检索,只剩下一个参数,即方法名称。
接下来,将展示如何在运行时调用方法。
默认情况下,知道 Bird 类的 walks 属性是假的。
想调用它的 setWalks 方法并将其设置为 true:
@Test
public void givenMethod_whenInvokes_thenCorrect() {
Class<?> birdClass = Class.forName("com.jack.yang.reflection.Bird");
Bird bird = (Bird) birdClass.getConstructor().newInstance();
Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
Method walksMethod = birdClass.getDeclaredMethod("walks");
boolean walks = (boolean) walksMethod.invoke(bird);
assertFalse(walks);
assertFalse(bird.walks());
setWalksMethod.invoke(bird, true);
boolean walks2 = (boolean) walksMethod.invoke(bird);
assertTrue(walks2);
assertTrue(bird.walks());
}
请注意如何首先调用 walks 方法并将返回类型强制转换为适当的数据类型,然后检索其值。稍后,调用 setWalks 方法来更改该值并再次测试。
9. 结论
在本文中,介绍了 Java Reflection API,并研究了如何使用它在运行时检索类、接口、字段和方法,而无需在编译时事先了解它们的内部结构。