• 游客, 欢迎您来到九域资源社区,如果您是新人,请前往 论坛公告 板块查看新人引导教程 或者 点我打开
    如果您发现没有下载许可, 请先验证邮箱再进行下载;金锭可通过每日登陆或资源出售获取,目前没有其他渠道可获取。

Java反射入门教程

豆浆123

Lv.1 泥土
高级创作者
2023-06-01
11
13
0
0.什么是反射?
反射是 Java 中一个非常强大的机制。
当我们需要编写一个 Java 程序时,我们需要知道并且引入我们需要使用的类,随后使用该类方法。
这种方式存在一个限制,即我们必须在编译时期就明确知道需要使用哪些类,并且必须在编译期将这些类的信息编码在程序中。
但是,在某些情况下,我们希望能够在运行时期动态获取并使用类,而不是事先硬编码在程序中。这时候,Java 反射机制就发挥作用了。
反射机制是指在运行时期获取和操作类的信息的能力,通过反射我们可以在程序运行时动态获取类的信息、动态创建对象、动态调用方法。
但需要注意的是,由于反射机制的灵活性,它可能导致代码的可读性和性能出现问题。
获取与更改私有字段
在 0 中我们已经讲解了反射的概念,接下来让我们简单的试试反射,获取一个类中的私有字段。

1.让我们先新建一个类,并添加私有字段 name,如下:
Makefile:
public class Example {
    private String name = "MorePokemon";
}

接下来让我们使用反射获取 Example 类中的私有字段 name。
首先我们需要获取到 Example 类的 Class 对象,我们可以通过 Class.forName() 进行获取。
Java:
Class<?> clazz = Class.forName("Example");

随后我们需要获取字段,使用 Class. getDeclaredField() 方法获取指定名称的字段,因为 name 字段是私有的,所以需要使用 getDeclaredField 方法。
Java:
Field nameField = clazz.getDeclaredField("name");
也许你可能会比较疑惑,为什么要用 getDeclaredField?

因为使用 getDeclaredField() 方法可以获取到类的所有字段,包括私有字段和公有字段。如果是公有字段,则可以使用 getField() 方法来获取。
那么这两个方法有什么不同? 其中getField() 只能获取类中的公有字段,如果要获取私有字段则会抛出NoSuchFieldException 异常。getDeclaredField() 可以获取类中的所有字段,包括公有字段和私有字段。
总的来说,如果要获取的字段是公有的,可以使用 getField() 方法,如果要获取的字段是私有的,就需要使用 getDeclaredField() 方法来获取。
接下来,由于 name 字段是私有的,需要通过 Field.setAccessible(true) 方法来设置访问权限为可访问,才能够获取和修改该字段的值。
Java:
nameField.setAccessible(true);

那么,再讲一下什么是 setAccessible() 方法,它是 Java 反射 API 中的一个方法,允许我们访问以及修改那些修饰符不允许的成员。
一般情况下,反射只能获取 public 成员,但是很显然我们肯定不想只局限于获取 public 成员,这时候我们就需要使用 setAccessible() 方法来解决这个问题。
接下来我们需要创建一个 Example 对象,随后进行获取,由于我们并没有给 Example 写构造器,如果我们没有给类写构造器,那么编译器会默认生成无参构造器。
也就是说我们需要进行无参构造。
Java:
Object obj = clazz.getDeclaredConstructor().newInstance();

在 JDK9 之后,Class.newInstance() 方法已经被废弃了,因为它不够灵活。现在应该使用 Class.getDeclaredConstructor().newInstance(),其中 getDeclaredConstructor() 可以传入构造方法的参数类型,或者不传入来获取默认的无参构造方法。
在上文我们已经写了 nameField 是 Example 类的私有字段 name,obj 则是创建的 Example 对象,接下来我们进行获取即可。我们使用 Field.get() 进行获取:
Java:
nameField.get(obj)

输出:
Java:
System.out.println(nameField.get(obj));

完整代码如下:
Java:
   public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);

        Object obj = clazz.getDeclaredConstructor().newInstance();

        System.out.println(nameField.get(obj));
    }

这看上去非常简单! 运行后输出MorePokemon,这是我们想要的结果!
接下来我们想要更改这个 name,我们只需要使用 nameField.set() 方法即可!
Java:
 nameField.set(obj, "MorePixelmon");

        System.out.println(nameField.get(obj));

输出为: MorePixelmon 这是我们想要的结果!
也许你可能会疑惑,Field 到底是什么东西? 接下来让我们简单的讲讲 Field。
Field 在 Java 中是一个反射类,表示一个类中的成员变量,使用 Field 对象可以获取和修改指定的成员变量,既使用 set get 等。
我们使用了 nameField.set(obj, "二百!"); 来进行设置,obj 实际上表示为被访问或修改的对象,通过 set 来设置 obj 指定的 Field 成员变量,get 来获取。

2.调用有参构造器
我们已经在 1 中学会了要怎么使用反射来访问成员变量,那么接下来让我们来调用有参构造器,让我们拓展 Example 类,为其添加一个构造器:
Java:
public class Example {
    private String name = "MorePokemon";
   
    public Example(String name) {
        this.name = name;
    }
}

我们已经为 Example 类添加了一个构造器,需要传入一个字符串类参数作为 name,接下来让我们使用反射调用它。
我们在 1 中已经知道了要如何调用无参构造器,这与调用有参构造器相差不大,只是在于参数传递不同。
我们在 Example 的构造器中需要传入一个 String 类型的参数:
Java:
  public Example(String name) {
        this.name = name;
    }

那么我们需要这样更改 obj 的创建:
Java:
Object obj = clazz.getDeclaredConstructor(String.class).newInstance("MorePixelmon");

在 getDeclaredConstructor 中我们需要规定传入的参数的类型,随后在 newInstance 中传入实际参数即可。
Java:
 public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);

        Object obj = clazz.getDeclaredConstructor(String.class).newInstance("MorePixelmon");

        System.out.println(nameField.get(obj));
    }

最后的输出为: MorePixelmon 这是我们想要的!
那么多参数应该如何传递? 接下来让我们再拓展一下 Example 类,为其添加一个新的字段:
Java:
ublic class Example {
    private String name = "MorePokemon";
    private int age = 114514;

    public Example(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

这实际上很简单,我们只需要继续传入即可:
Java:
Object obj = clazz.getDeclaredConstructor(String.class, int.class).newInstance("MorePixelmon", 1);

接下来让我们再创建一个 Field 对象,为 age 字段,并且获取 obj 中的 age 字段输出。
Java:
  public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        Field ageField = clazz.getDeclaredField("age");

        nameField.setAccessible(true);
        ageField.setAccessible(true);

        Object obj = clazz.getDeclaredConstructor(String.class, int.class).newInstance("MorePixelmon", 1);

        System.out.println(nameField.get(obj));
        System.out.println(ageField.get(obj));
    }

输出为:
MorePixelmon
1
这是我们想要的


3.调用私有方法
在 1 和 2 中我们已经知道了如何访问私有字段以及更改私有字段、调用无参或有参构造器,那么接下来让我们学学如何调用私有方法。
还是一样,让我们先拓展 Example 类,让我们为其写两个方法,用来设置 name 以及 age:
Java:
public class Example {
    private String name = "MorePokemon";
    private int age = 114514;

    public Example(String name, int age) {
        this.name = name;
        this.age = age;
    }
   
    private void setName(String name) {
        this.name = name;
    }
   
    private void setAge(int age) {
        this.age = age;
    }
}

那么接下来,让我们接触一个新的类 – Method。
Method 代表了类的方法,每个方法都是一个 Method 对象,它包含方法名称、返回类型、参数、修饰符等等信息,我们可以使用 invoke() 方法调用一个对象的方法。
我们可以使用 Class.getMethod() 来获得 public 类型 Method 对象,如果是 private,那么和 getDeclaredField() 一样,我们使用 getDeclaredMethod() 即可,由于 setName() 方法是私有的,那么我们使用 getDeclaredMethod()。
getDeclaredMethod() 方法需要传入对象的方法名称,以及参数类型:
Java:
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);

这段代码我们创建了 setNameMethod 对象,使用 getDeclaredMethod() 获取私有方法,由于 setName() 需要传入一个 String 类型参数,所以此处进行规定。
另外别忘了,由于 setName() 方法是私有,所以我们还需要用到 setAccessible(true) 更改访问权限,否则会抛出 IllegalAccessException 异常。
Java:
setNameMethod.setAccessible(true);

完整代码:
Java:
 public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        Field ageField = clazz.getDeclaredField("age");

        Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);

        nameField.setAccessible(true);
        ageField.setAccessible(true);
        setNameMethod.setAccessible(true);

        Object obj = clazz.getDeclaredConstructor(String.class, int.class).newInstance("MorePokemon", 1);

        setNameMethod.invoke(obj, "MorePokemon");

        System.out.println(nameField.get(obj));
        System.out.println(ageField.get(obj));
    }

输出为:
MorePokemon
1
可以看到名字已经被正确的更改了,那么我们使用相同的方法来更改一下 age:
Java:
public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        Field ageField = clazz.getDeclaredField("age");

        Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
        Method setAgeMethod = clazz.getDeclaredMethod("setAge", int.class);

        nameField.setAccessible(true);
        ageField.setAccessible(true);

        setNameMethod.setAccessible(true);
        setAgeMethod.setAccessible(true);

        Object obj = clazz.getDeclaredConstructor(String.class, int.class).newInstance("MorePixelmon", 1);

        setNameMethod.invoke(obj, "MorePixelmon");
        setAgeMethod.invoke(obj, 114);

        System.out.println(nameField.get(obj));
        System.out.println(ageField.get(obj));
    }

输出为:
MorePixelmon
114
这是我们想要的!

4.反射获取注解
在 1 2 3 中我们已经基本入门简单的反射了,那么接下来我们来学学要如何通过反射来获取注解,老样子,让我们拓展 Example 类,并且写一个自定义注解类:
这是一个自定义注解类,此注解没有实际作用,只是测试用:
Java:
public class Annotation {
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        String value();
    }
}

首先我们需要意识到一点,注解是有保留策略的,其中保留策略既为 RetentionPolicy 注解规定,保留策略有三种:
1.SOURCE - 注释将被编译器丢弃
2.CLASS - 注释由编译器记录在类文件中,但不需要由 VM 在运行时保留。 这是默认
3.RUNTIME - 注释由编译器记录在类文件中,并在运行时由 VM 保留,因此它们可以被反射读取

其中只有 RUNTIME 保留策略的注解可以被反射读取,因为只有 RUNTIME 在运行时由 VM 进行保留,SOURCE 保留策略注解将在编译时被丢弃,而 CLASS 会将注解信息编译到生成的 .class 文件中,但在运行时期间不会将注解信息加载到内存中。
这就是为什么有的注解无法通过反射获取,比如 @SuppressWarnings。
@SuppressWarnings 是一个编译时注解,它的保留策略是 RetentionPolicy.SOURCE,意味着在编译后它会被丢弃,因此在运行时使用反射时无法获取它。
随后让我们给字段以及方法使用 MyAnnotation 注解
Java:
public class Example {
   @Annotation.MyAnnotation("1")
    private String name = "MorePokemon";

    @Annotation.MyAnnotation("2")
    private int age = 114514;

    @Annotation.MyAnnotation("3")
    public Example(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Annotation.MyAnnotation("4")
    private void setName(String name) {
        this.name = name;
    }

    @Annotation.MyAnnotation("5")
    private void setAge(int age) {
        this.age = age;
    }
}

我们只需要使用 getAnnotation() 方法获取注解即可,Field、Method、Class 都具有此方法,让我们先获取 name 以及 age 字段的注解:
Java:
        Annotation.MyAnnotation nameFieldMyAnnotation = nameField.getAnnotation(Annotation.MyAnnotation.class);
        Annotation.MyAnnotation ageFieldMyAnnotation = ageField.getAnnotation(Annotation.MyAnnotation.class);

我们只要创建一个新的注解对象,并且使用 getAnnotation() 方法即可,getAnnotation() 方法内填入注解类。
需要注意的是,如果对应方法没有注解,那么此注解对象为空,在使用前需要进行空判断,让我们进行输出:
Java:
public static void main(String[] args) throws Exception {
       Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        Field ageField = clazz.getDeclaredField("age");

        nameField.setAccessible(true);
        ageField.setAccessible(true);

        Annotation.MyAnnotation nameFieldMyAnnotation = nameField.getAnnotation(Annotation.MyAnnotation.class);
        Annotation.MyAnnotation ageFieldMyAnnotation = ageField.getAnnotation(Annotation.MyAnnotation.class);

        if (nameFieldMyAnnotation != null) {
            System.out.println(nameFieldMyAnnotation.value());
        }

        if (ageFieldMyAnnotation != null) {
            System.out.println(ageFieldMyAnnotation.value());
        }
    }

我们这里获取了 nameField 以及 ageField 的 MyAnnotaion 注解值,输出为:
1
2
这是我们想要的!

Java:
  @Annotation.MyAnnotation("1")
    private String name = "MP";

    @Annotation.MyAnnotation("2")
    private int age = 114514;

name 与 age 的注解值确实分别为 1 2,是正确的!
接下来让我们获取方法的注解值:
Java:
Annotation.MyAnnotation setNameMethodMyAnnotation = setNameMethod.getAnnotation(Annotation.MyAnnotation.class);

进行输出:
Java:
  public static void main(String[] args) throws Exception {
         Class<?> clazz = Class.forName("Example");

        Field nameField = clazz.getDeclaredField("name");
        Field ageField = clazz.getDeclaredField("age");

        Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);

        nameField.setAccessible(true);
        ageField.setAccessible(true);

        setNameMethod.setAccessible(true);

        Annotation.MyAnnotation setNameMethodMyAnnotation = setNameMethod.getAnnotation(Annotation.MyAnnotation.class);

        if (setNameMethodMyAnnotation != null) {
            System.out.println(setNameMethodMyAnnotation.value());
        }
    }

输出为: 4
Java:
    @Annotation.MyAnnotation("4")
    private void setName(String name) {
        this.name = name;
    }

这是我们想要的!

5.反射获取注解
在上面的 1 2 3 4 教程中都是非静态字段与方法,那么接下来我们应该如何访问静态字段与方法,这很简单! 让我们新建一个静态类 StaticExample:
Java:
public class StaticExample {
  @Annotation.MyAnnotation("1")
    private static String name = "MP";

    @Annotation.MyAnnotation("2")
    private static int age = 114514;

    @Annotation.MyAnnotation("3")
    private static void send(String string) {
        System.out.println(string);
    }
}

其中有两个静态私有字段,为 name 与 age,一个静态私有方法 send,需要传入 String 类型参数。
我们只需要让 Object 为 null 调用即可:
Java:
 public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("StaticExample");

        Field nameField = clazz.getDeclaredField("name");
        Field ageField = clazz.getDeclaredField("age");

        Method send = clazz.getDeclaredMethod("send", String.class);

        nameField.setAccessible(true);
        ageField.setAccessible(true);

        send.setAccessible(true);

        send.invoke(null, "send!");

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

        System.out.println(nameField.get(null));
        System.out.println(ageField.get(null));

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

        nameField.set(null, "二百!!!!!");
        ageField.set(null, 114);

        System.out.println(nameField.get(null));
        System.out.println(ageField.get(null));

输出为:
send!
---------------------
MP
114514
---------------------
MP
114
这是我们想要的!

6.反射获取父类
我们可以通过 Class. getSuperclass() 方法来获取父类,让我们先写几个示例类:
Java:
public class Animal {
  private String name;

    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat(){
        System.out.println(name + " eat");
    }

    public void sleep(){
        System.out.println(name + " sleep");
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
Java:
public class Cat extends Animal {
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meow() {
        System.out.println(super.getName() + " meow!");
    }
}

可以看到,Cat 类继承了 Animal,让我们通过反射获取 Cat 的父类:
Java:
  public static void main(String[] args) throws Exception {
          Class<?> clazz = Class.forName("Animals.Cat");
        Class<?> superClass = clazz.getSuperclass();

        System.out.println("Super class: " + superClass.getName());
    }

输出为: Super class: Animals.Animal
这是我们想要的!

7.反射获取接口
我们可以通过 Class. getInterfaces () 方法来获取父类,让我们先写几个示例类:
Java:
public interface Animal {
    void eat();
    void sleep();
}


public class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("eat");
    }

    @Override
    public void sleep() {
        System.out.println("sleep");
    }
}

可以看到,Cat 实现了 Animal 接口,让我们通过反射获取 Cat 类的接口:
Java:
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Animals.Cat");

        for (Class<?> interfaceClass : clazz.getInterfaces()) {
            System.out.println("Interface: " + interfaceClass.getName());
        }
    }

输出为: Interface: Animals.Animal
这是我们想要的!

8.反射获取和操作类加载器
反射操作类加载器主要用于动态加载类、拓展程序、热部署、安全管理等等场景,可以使得程序拓展性与可维护性更强。
示例:
Java:
public class Test {
    private String name;

    public Test(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
Java:
   public static void main(String[] args) throws Exception {
        // 获取当前线程的类加载器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // 加载Test类
        Class<?> clazz = classLoader.loadClass("com.example.Test");

        // 获取Test类的构造器
        Constructor<?> constructor = clazz.getConstructor(String.class);

        // 创建Test类的实例
        Object obj = constructor.newInstance("Hello World");

        // 调用Test类的getName方法
        String name = (String) clazz.getMethod("getName").invoke(obj);

        System.out.println("name: " + name);
    }

输出为: name: Hello World

以上代码通过反射获取当前线程的类加载器,用类加载器加载 Test 类,获取构造器并且创建实例,获取名称输出结果。
为什么用 loadClass 而不是直接 Class.fromName()?
这两种方法都是返回与指定类名对应的 Class 对象,但是它们之间存在一些差别:
Class.forName() 方法会触发类的初始化,而 ClassLoader.loadClass() 方法则不会。
在实际开发中,通常优先使用 ClassLoader.loadClass() 方法,因为它可以避免不必要的类初始化,提高程序的性能。
使用 ClassLoader.loadClass() 方法还可以实现更加灵活的类加载策略。可以通过自定义类加载器来实现自己的类加载逻辑,加载加密过的类、从远程服务器上加载类等。

9.反射获取和操作枚举类型
使用反射获取和操作枚举类型的方法与普通类类似,不过需要注意一些特殊的地方,以下是实例:
Java:
public enum EnumsTest {
    TEST("TEST :D"),

    TEST2("TEST2 :D");

    private final String value;

    EnumsTest(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

Java:
public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("EnumsTest");
        
              Object[] enumConstants = clazz.getEnumConstants();

        // 获取枚举常量的名称
        String name = ((Enum<?>) enumConstants[1]).name();

        // 获取枚举常量的序号
        int ordinal = ((Enum<?>) enumConstants[1]).ordinal();

        // 使用 getValue 获取属性值
        String value = (String) clazz.getMethod("getValue").invoke(enumConstants[1]);

        System.out.println(name);
        System.out.println(ordinal);
        System.out.println(value);
    }

输出为:
TEST2
1
TEST2 :D
这是我们想要的!

10.性能问题
反射提供了强大的灵活性,但是对性能有一定的影响,由于反射调用方法、访问字段等操作是在运行时动态解析的,所以它们不会被 JIT 编译器优化,导致反射操作比直接调用方法等操作更慢。
且可能会破坏封装性,因为它可以访问和修改私有方法、字段等成员。这会导致安全性问题,并增加代码的维护难度。
谨慎使用反射、避免在需要高性能的代码中频繁使用反射操作。
如果您看完了本教程,那么您应该已经算是入门反射了,恭喜! :)