反射概述

在本文中,我们将探讨Java反射,它允许我们检查或修改类、接口、字段和方法的运行时属性。当我们在编译时不知道它们的名称时,这尤其有用。

此外,我们可以实例化新对象、调用方法以及使用反射获取或设置字段值。

反射项目设置

要使用Java反射,我们不需要包括任何特殊的jar、任何特殊的配置或Maven依赖项。JDK附带了一组绑定在java.lang.reflect专门针对此目的的包装。

因此,我们需要做的就是在代码中导入以下内容:

import java.lang.reflect.*;

我们可以开始了。

为了访问实例的类、方法和字段信息,我们调用getClass方法,该方法返回对象的运行时类表示。返回的类对象提供了访问类信息的方法。

反射例子

为了进一步了解,我们将看一个非常基本的示例,它在运行时检查一个简单Java对象的字段。

让我们创建一个简单的Person类,只包含nameage字段,根本没有方法。以下是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对象数组,即使对象的引用是该对象的父类型。

在上面的例子中,我们只对这些字段的名称感兴趣,但是还有很多事情可以做,我们将在后面的章节中看到更多的例子。

注意我们如何使用helper方法来提取实际的字段名,这是一个非常基本的代码:

private static List<String> getFieldNames(Field[] fields) {
    List<String> fieldNames = new ArrayList<>();
    for (Field field : fields)
      fieldNames.add(field.getName());
    return fieldNames;
}

Java反射用例

在讨论Java反射的不同特性之前,我们将讨论它的一些常见用途。Java反射功能非常强大,可以通过多种方式非常方便地使用。

例如,在许多情况下,我们对数据库表有一个命名约定。我们可以选择通过预先固定表名来增加一致性,这样一个包含学生数据的表被称为tbl_student_data

在这种情况下,我们可以将保存学生数据的Java对象命名为studentStudentData。然后使用CRUD范式,我们为每个操作都有一个入口点,这样创建操作只接收一个对象参数。

然后我们使用反射来检索对象名和字段名。此时,我们可以将此数据映射到DB表,并将对象字段值分配给适当的DB字段名。比如MyBatis框架就大量使用了反射原理。

检查Java类

在本节中,我们将探讨javareflectionapi中最基本的组件。如前所述,Java类对象使我们能够访问任何对象的内部细节。

我们将检查内部细节,如对象的类名、修饰符、字段、方法、实现的接口等。

为了牢牢掌握反射API,我们将创建一个抽象的Animal类来实现Eating接口,因为它应用于Java类并有各种各样的示例。这个接口定义了我们创建的任何具体动物对象的进食行为。

首先,这里是吃东西的界面:

public interface Eating {
    String eats();
}

然后是进食界面的具体动物实现:

public abstract class Animal implements Eating {
 
    public static String CATEGORY = "domestic";
    private String name;
 
    protected abstract String getSound();
 
    // constructor, standard getters and setters omitted 
}

我们还可以创建另一个名为motion的界面,它描述了动物如何移动:

public interface Locomotion {
    String getLocomotion();
}

现在我们将创建一个名为Goat的具体类,它扩展了Animal并实现了移动。由于超类实现了吃,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对象的各个方面。

类名 Class Names

首先从类中获取对象的名称:

@Test
public void givenObject_whenGetsClassName_thenCorrect() {
    Object goat = new Goat("goat");
    Class<?> clazz = goat.getClass();
 
    assertEquals("Goat", clazz.getSimpleName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getCanonicalName());
}

注意,类的getSimpleName方法返回对象的基本名称,就像它在声明中显示的那样。然后其他两个方法返回完全限定的类名,包括包声明。

让我们看看,如果我们只知道Goat类的完全限定类名,我们如何创建Goat类的对象:

@Test
public void givenClassName_whenCreatesObject_thenCorrect(){
    Class<?> clazz = Class.forName("com.baeldung.reflection.Goat");
 
    assertEquals("Goat", clazz.getSimpleName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getCanonicalName()); 
}

注意,我们传递给静态forName方法的名称应该包含包信息。否则,我们将得到一个ClassNotFoundException

类修饰符 Class Modifiers

我们可以通过调用返回整数的getModifiers方法来确定类中使用的修饰符。每个修饰符都是一个标志位,可以设置或清除。

这个java.lang.reflect.Modifier类提供静态方法,用于分析返回的整数是否存在特定修饰符。

让我们确认一下上面定义的一些类的修饰符:

@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
    Class<?> goatClass = Class.forName("com.baeldung.reflection.Goat");
    Class<?> animalClass = Class.forName("com.baeldung.reflection.Animal");
 
    int goatMods = goatClass.getModifiers();
    int animalMods = animalClass.getModifiers();
 
    assertTrue(Modifier.isPublic(goatMods));
    assertTrue(Modifier.isAbstract(animalMods));
    assertTrue(Modifier.isPublic(animalMods));
}

我们可以检查库jar中任何类的修饰符,这些类是我们要导入到项目中的。

在大多数情况下,我们可能需要使用forName方法而不是完整的实例化,因为在内存密集类的情况下,这将是一个昂贵的过程。

包装信息

通过使用Java反射,我们还能够获得关于任何类或对象的包的信息。这些数据绑定在Package类中,Package类是通过调用class对象上的getPackage方法返回的。

@Test
public void givenClass_whenGetsPackageInfo_thenCorrect() {
    Goat goat = new Goat("goat");
    Class<?> goatClass = goat.getClass();
    Package pkg = goatClass.getPackage();
 
    assertEquals("com.baeldung.reflection", pkg.getName());
}

超类 Super Class

我们还可以通过使用Java反射来获得任何Java类的超类。

在许多情况下,特别是在使用库类或Java的内置类时,我们可能事先不知道所使用对象的超类,本小节将介绍如何获取这些信息。

所以让我们来确定山羊的超类。此外,我们还展示了java.lang.String类是的子类java.lang.Object班级:

@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());
}

实现的接口

使用Java反射,我们还可以获得给定类实现的接口列表。

让我们检索由Goat类和Animal abstract类实现的接口的类类型:

@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
    Class<?> goatClass = Class.forName("com.baeldung.reflection.Goat");
    Class<?> animalClass = Class.forName("com.baeldung.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实现了移动,而Animal实现了进食,正如代码中所显示的那样。

您可能已经注意到Goat是抽象类Animal的一个子类,并且实现了接口方法eats(),那么Goat还实现了Eating接口。

因此,值得注意的是,只有类显式声明为使用implements关键字实现的接口才会出现在返回的数组中。

因此,即使一个类实现接口方法是因为它的超类实现了该接口,但子类没有用implements关键字直接声明该接口,那么该接口也不会出现在接口数组中。

构造函数、方法和字段

使用Java反射,我们可以检查任何对象类的构造函数以及方法和字段。

稍后,我们将能够看到对一个类的每个组件进行更深入的检查,但是现在,只需获取它们的名称并将它们与我们期望的进行比较就足够了。

让我们看看如何获取Goat类的构造函数:

@Test
public void givenClass_whenGetsConstructor_thenCorrect(){
    Class<?> goatClass = Class.forName("com.baeldung.reflection.Goat");
 
    Constructor<?>[] constructors = goatClass.getConstructors();
 
    assertEquals(1, constructors.length);
    assertEquals("com.baeldung.reflection.Goat", constructors[0].getName());
}

我们也可以这样检查动物类的领域:

@Test
public void givenClass_whenGetsFields_thenCorrect(){
    Class<?> animalClass = Class.forName("com.baeldung.reflection.Animal");
    Field[] fields = animalClass.getDeclaredFields();
 
    List<String> actualFields = getFieldNames(fields);
 
    assertEquals(2, actualFields.size());
    assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
}

就像我们可以检查动物类的方法一样:

@Test
public void givenClass_whenGetsMethods_thenCorrect(){
    Class<?> animalClass = Class.forName("com.baeldung.reflection.Animal");
    Method[] methods = animalClass.getDeclaredMethods();
    List<String> actualMethods = getMethodNames(methods);
 
    assertEquals(4, actualMethods.size());
    assertTrue(actualMethods.containsAll(Arrays.asList("getName",
      "setName", "getSound")));
}

getFieldNames一样,我们添加了一个helper方法来从方法对象数组中检索方法名:

private static List<String> getMethodNames(Method[] methods) {
    List<String> methodNames = new ArrayList<>();
    for (Method method : methods)
      methodNames.add(method.getName());
    return methodNames;
}

反射的更多作用也可以参考这篇文章:https://javakk.com/679.html