Java 反射(Reflection)学习笔记

一、反射的核心概念

反射是 Java 语言的核心特性之一,属于 Java 高级开发必备知识点,它允许程序在运行时(而非编译时)获取类的所有信息(类名、属性、方法、构造器等),并且能够动态操作类的实例、调用方法、修改属性,无需在编译期明确知道具体的类信息。

简单来说:反射打破了 Java 的封装性(可以访问私有成员),让程序拥有“自检查”和“动态操作”的能力,是框架(Spring、MyBatis 等)的核心底层原理。

二、反射的核心前提

要使用反射,必须先获取目标类的 Class 对象——Class 类是反射的入口,它代表 Java 中所有的类和接口,每个类在 JVM 中只会有一个Class 对象(无论通过哪种方式获取)。

JVM 加载类时,会自动创建该类的 Class 对象,并存放在方法区,反射就是通过这个对象,解锁类的所有信息。

三、获取 Class 对象的 3 种方式(重点)

三种方式的优先级和使用场景不同,务必区分清楚,避免混淆。

方式 1:通过类名.class 获取(最安全、最常用)

适用场景:编译期已知目标类(明确知道类名),无异常抛出,无需处理异常。

// 语法:类名.class
Class<Student> clazz1 = Student.class;
Class<String> clazz2 = String.class;

方式 2:通过对象.getClass() 获取

适用场景:已有类的实例对象,通过实例反向获取 Class 对象,需处理 NullPointerException(对象不能为 null)。

// 语法:对象.getClass()
Student student = new Student();
Class<? extends Student> clazz3 = student.getClass();

方式 3:通过 Class.forName(String 全类名) 获取(最灵活)

适用场景:编译期未知目标类,通过字符串形式的“全类名”(包名+类名)动态加载,需处理 ClassNotFoundException(全类名错误会抛出)。

注意:全类名必须完整(如 java.lang.String,不能只写 String),是框架中最常用的方式(通过配置文件读取全类名,动态加载类)。

// 语法:Class.forName("全类名")
try {
    Class<?> clazz4 = Class.forName("com.test.Student");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

四、反射的核心 API(java.lang.reflect 包)

获取 Class 对象后,通过以下核心类操作类的成员,均位于 java.lang.reflect 包下(需导入):

核心类

作用

Field

封装类的属性(成员变量),可获取/修改属性值(包括私有属性)

Method

封装类的方法,可动态调用方法(包括私有方法)

Constructor

封装类的构造器,可动态创建类的实例(包括私有构造器)

Modifier

解析类/属性/方法的修饰符(如 public、private、static 等)

五、反射的常见操作(实战重点)

以下操作均以 Student 类为示例,先定义目标类:

package com.test;

public class Student {
    // 成员属性(public + private)
    public String name;
    private int age;
    // 静态属性
    public static String school = "安徽师范大学";

    // 构造器(无参 + 有参 + 私有)
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    private Student(String name) {
        this.name = name;
    }

    // 成员方法(public + private)
    public void study() {
        System.out.println(name + "正在学习");
    }
    private void eat(String food) {
        System.out.println(name + "在吃" + food);
    }

    // 静态方法
    public static void showSchool() {
        System.out.println("学校:" + school);
    }

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
}

操作 1:通过 Class 对象创建类的实例(3 种方式)

方式 1:通过无参构造器创建(最常用)

语法:clazz.newInstance()(JDK9 后过时,推荐用 getConstructor().newInstance()

try {
    // 1. 获取 Class 对象
    Class<?> clazz = Class.forName("com.test.Student");
    // 2. 通过无参构造器创建实例(需处理 2 个异常)
    Student student = (Student) clazz.getConstructor().newInstance();
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
    e.printStackTrace();
}

方式 2:通过有参构造器创建

步骤:先获取指定参数类型的构造器(Constructor),再调用 newInstance(参数)

try {
    Class<?> clazz = Class.forName("com.test.Student");
    // 获取有参构造器(参数类型:String.class, int.class)
    Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
    // 传入参数,创建实例
    Student student = (Student) constructor.newInstance("张三", 20);
    System.out.println(student); // 输出:Student{name='张三', age=20}
} catch (Exception e) {
    e.printStackTrace();
}

方式 3:通过私有构造器创建(打破封装)

注意:私有构造器需用 getDeclaredConstructor() 获取(区别于 getConstructor()),且需调用 setAccessible(true) 取消访问检查。

try {
    Class<?> clazz = Class.forName("com.test.Student");
    // 获取私有构造器(参数类型:String.class)
    Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
    // 取消访问检查(关键:否则无法访问私有构造器,抛出 IllegalAccessException)
    constructor.setAccessible(true);
    // 创建实例
    Student student = (Student) constructor.newInstance("李四");
    System.out.println(student); // 输出:Student{name='李四', age=0}
} catch (Exception e) {
    e.printStackTrace();
}

操作 2:获取并操作类的属性(Field)

1. 获取属性(4 种方法)

Class<?> clazz = Class.forName("com.test.Student");

// 1. 获取指定名称的 public 属性(包括父类的 public 属性)
Field nameField = clazz.getField("name");

// 2. 获取指定名称的所有属性(包括 private、default、protected,不包括父类)
Field ageField = clazz.getDeclaredField("age");

// 3. 获取所有 public 属性(包括父类)
Field[] publicFields = clazz.getFields();

// 4. 获取所有属性(包括 private,不包括父类)
Field[] allFields = clazz.getDeclaredFields();

2. 操作属性(获取值、修改值)

注意:操作私有属性,必须先调用 setAccessible(true) 取消访问检查。

try {
    Class<?> clazz = Class.forName("com.test.Student");
    Student student = (Student) clazz.getConstructor().newInstance();

    // 操作 public 属性(name)
    Field nameField = clazz.getField("name");
    nameField.set(student, "子滔"); // 修改属性值:set(实例对象, 新值)
    String name = (String) nameField.get(student); // 获取属性值:get(实例对象)
    System.out.println(name); // 输出:王五

    // 操作 private 属性(age)
    Field ageField = clazz.getDeclaredField("age");
    ageField.setAccessible(true); // 取消访问检查(关键)
    ageField.set(student, 22);
    int age = (int) ageField.get(student);
    System.out.println(age); // 输出:22

    // 操作 static 属性(school)—— 实例对象可传 null
    Field schoolField = clazz.getField("school");
    schoolField.set(null, "安徽师范大学大学"); // static 属性无需实例
    String school = (String) schoolField.get(null);
    System.out.println(school); // 输出:清华大学
} catch (Exception e) {
    e.printStackTrace();
}

操作 3:获取并调用类的方法(Method)

1. 获取方法(4 种方法)

Class<?> clazz = Class.forName("com.test.Student");

// 1. 获取指定名称、指定参数类型的 public 方法(包括父类)
Method studyMethod = clazz.getMethod("study");

// 2. 获取指定名称、指定参数类型的所有方法(包括 private,不包括父类)
Method eatMethod = clazz.getDeclaredMethod("eat", String.class);

// 3. 获取所有 public 方法(包括父类)
Method[] publicMethods = clazz.getMethods();

// 4. 获取所有方法(包括 private,不包括父类)
Method[] allMethods = clazz.getDeclaredMethods();

2. 调用方法

语法:method.invoke(实例对象, 方法参数),调用私有方法需先 setAccessible(true)

try {
    Class<?> clazz = Class.forName("com.test.Student");
    Student student = (Student) clazz.getConstructor().newInstance();
    student.setName("赵六"); // 先给 name 赋值

    // 调用 public 无参方法(study)
    Method studyMethod = clazz.getMethod("study");
    studyMethod.invoke(student); // 输出:赵六正在学习

    // 调用 private 有参方法(eat)
    Method eatMethod = clazz.getDeclaredMethod("eat", String.class);
    eatMethod.setAccessible(true); // 取消访问检查
    eatMethod.invoke(student, "米饭"); // 输出:赵六在吃米饭

    // 调用 static 方法(showSchool)—— 实例对象可传 null
    Method showSchoolMethod = clazz.getMethod("showSchool");
    showSchoolMethod.invoke(null); // 输出:学校:清华大学(之前修改过 static 属性)
} catch (Exception e) {
    e.printStackTrace();
}

六、反射的优缺点

优点

  • 灵活性高:可动态加载类、创建实例、调用方法,无需编译期明确类信息,适合框架开发(如 Spring 依赖注入、MyBatis 映射)。

  • 可操作性强:打破封装性,能访问和操作类的私有成员,满足特殊业务需求。

  • 扩展性好:可通过配置文件(如 XML、properties)动态切换类,无需修改代码,降低耦合度。

缺点

  • 性能损耗:反射操作需要 JVM 解析 Class 对象、取消访问检查,比直接调用(非反射)慢,频繁使用会影响程序性能。

  • 安全性降低:打破封装性,可能误操作私有成员,导致程序逻辑混乱;若反射代码被恶意篡改,会存在安全风险。

  • 可读性差:反射代码相对繁琐,动态操作无法通过代码直观判断,增加后期维护成本。

七、反射的使用场景

  1. 框架开发(核心场景):Spring 的 IOC(依赖注入)、MyBatis 的 SQL 映射、Hibernate 的ORM 映射,均通过反射动态加载类和实例。

  2. 动态代理:Java 动态代理(JDK 代理)的核心是通过反射调用目标方法(如 Spring AOP)。

  3. 工具类开发:如 IDE 的代码提示、JUnit 单元测试框架(通过反射调用测试方法)、JSON 解析工具(如 FastJSON,通过反射获取属性并序列化)。

  4. 特殊业务场景:需要动态加载未知类、操作私有成员的场景(如插件化开发、框架扩展)。

八、注意事项(避坑重点)

  • 尽量避免频繁使用反射:若需频繁调用,可缓存 Class 对象、Method 对象、Field 对象,减少 JVM 解析开销。

  • 操作私有成员时,务必调用 setAccessible(true),但该方法仅取消访问检查,不能改变成员的私有修饰符本质。

  • 反射代码需处理大量异常(如 ClassNotFoundException、NoSuchMethodException 等),建议统一捕获或抛出,避免程序崩溃。

  • JDK 9 及以上版本,反射访问模块内的类时,需在 module-info.java 中添加 opens 包名 to 反射使用的模块,否则会抛出访问限制异常。

  • 避免用反射操作不安全的代码:反射的灵活性可能被滥用,导致代码可维护性下降,尽量在必要时使用。

九、总结

1. 反射的核心是 Class 对象,入口是获取 Class 对象的 3 种方式;

2. 反射的核心操作:动态创建实例(Constructor)、操作属性(Field)、调用方法(Method);

3. 反射的核心价值:为框架提供动态性和灵活性,是 Java 高级开发的基础;

4. 使用原则:按需使用,兼顾性能和安全性,避免滥用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值