浅谈反射机制

1. 何为反射?

反射(Reflection)机制指的是程序在运行的时候能够获取自身的信息。具体来说,反射允许程序在运行时获取关于自己代码的各种信息。如果知道一个类的名称或者它的一个实例对象, 就能把这个类的所有方法和变量的信息(方法名,变量名,方法,修饰符,类型,方法参数等等所有信息)找出来。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。简单来说,就是面向对象看待一个类,即以面向对象的思想来抽象一个类。

反射是一种强大的功能,广泛应用于框架设计、依赖注入、单元测试和动态代理等领域。然而,由于反射会绕过编译时的类型安全检查,所以使用不当可能会导致更高的运行时错误几率和性能开销。因此,在使用反射时需要格外谨慎,以避免潜在的风险。

2. 反射的意义所在?

  1. 通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类。
  2. 使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法。
  3. 反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。

不过反射也存在明显的缺点:使用反射性能较低,需要解析字节码,且如果将内存中的对象进行解析,相对不安全,会破坏封装性。

3. 通过反射实例化对象

在Java中,可以通过反射获取类的构造器、方法、字段等信息:

java 复制代码
class Test {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {

        // 下面这段代码是以反射机制的方式创建对象。

        // 通过反射机制,获取Class,通过Class来实例化对象
        Class c = Class.forName("com.reflectBean.User");

        // newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
        // 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
        Object obj = c.newInstance();

        Method method = c.getMethod("myMethod");
        method.invoke(obj);
    }
}

4. 获取Class类的三种方式

在Java编程中,获取 Class 对象的三种常用方式如下:

  1. 通过类名的静态属性 class: 这是获取 Class 对象最简单的方法,适用于已知编译时类型的情况。
java 复制代码
Class<MyClass> clazz = MyClass.class;
  1. 通过对象的实例方法 getClass: 这是在运行时获取一个对象的 Class 对象的方法,适用于已知一个实例化对象的情况。
java 复制代码
MyClass obj = new MyClass();
Class<? extends MyClass> clazz = obj.getClass();
  1. 通过类名的字符串 forName: 这是动态加载类的一种方式,特别有用在类名只有在运行时才能确定的情况下。
java 复制代码
try {
    Class<?> clazz = Class.forName("com.reflectBean.User");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

5. 常用的Class类的方法

Class 类在 Java 中定义了大量的方法,可以用来反射性地获取类的各种信息或进行操作。不过需要注意的是,在使用反射进行私有成员访问时,需要设置相应的访问权限,如 setAccessible(true)。以下是一些常用的方法:

  1. 类信息获取方法
  • getName():返回完整类名带包名。

  • getSimpleName():获取类的简单名称。

  • getCanonicalName():获取类的规范名称。

  • getPackage():获取类所在的包。

  • getModifiers():获取类的修饰符(如 public, abstract, final 等)。

  1. 构造函数相关方法
  • getConstructor(Class<?>... parameterTypes):获取指定参数类型的公共构造函数。

  • getConstructors():获取所有公共构造函数。

  • getDeclaredConstructor(Class<?>... parameterTypes):获取指定参数类型的任意(包括私有)构造函数。

  • getDeclaredConstructors():获取所有任意(包括私有)构造函数。

  1. 方法相关方法
  • getMethod(String name, Class<?>... parameterTypes):获取指定名称和参数类型的公共方法。
  • getMethods():获取所有公共方法,包括从父类继承的方法。
  • getDeclaredMethod(String name, Class<?>... parameterTypes):获取指定名称和参数类型的任意方法。
  • getDeclaredMethods():获取所有任意方法(包括私有)。
  1. 字段(成员变量)相关方法
  • getField(String name):获取指定名称的公共字段。
  • getFields():获取所有公共字段。
  • getDeclaredField(String name):获取指定名称的任意字段。
  • getDeclaredFields():获取所有任意字段(包括私有)。
  1. 类层次结构相关方法
  • getSuperclass():获取父类。
  • getInterfaces():获取实现的接口。
  • isAssignableFrom(Class<?> cls):判断当前类或接口是否可以从指定类型的对象赋值(即能否进行类型转换)。
  1. 实例创建方法
  • newInstance():创建类的实例,需要无参构造函数。
  1. 其他方法
  • getAnnotations():获取类上的注解。
  • isAnnotation():判断类是否是注解类型。
  • isInterface():判断是否为接口。
  • isArray():判断是否为数组。
  • isPrimitive():判断是否为基本数据类型。
  • isInstance(Object obj):判断给定对象是否为该 Class 的实例。

6. 获取Field的四种方式

在Java中,可以通过Class对象获取某个类的字段(Field),Field类代表类的成员变量。获取 Field的几种常用方式如下:

  1. 通过字段名获取公共字段 (public 属性):
java 复制代码
try {
    Class<?> clazz = Class.forName("com.reflectBean.User");
    Field field = clazz.getField("userName");
    // 对 field 进行操作
    // ...

} catch (NoSuchFieldException | ClassNotFoundException e) {
    e.printStackTrace();
}
  1. 获取所有公共字段 (public 属性):
java 复制代码
try {
    Class<?> clazz = Class.forName("com.reflectBean.User");
    Field[] fields = clazz.getFields();
    for (Field field : fields) {
        // 对每个 field 进行操作
        // ...    
    }
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
  1. 通过字段名获取任何字段(包括私有、保护和包访问属性):
java 复制代码
try {
    Class<?> clazz = Class.forName("com.reflectBean.User");
    Field field = clazz.getDeclaredField("password");
    // 允许访问私有字段
    field.setAccessible(true);  
    // 对 field 进行操作,比如设置可访问性
    field.setAccessible(true);
} catch (NoSuchFieldException | ClassNotFoundException e) {
    e.printStackTrace();
}
  1. 获取所有字段(包括私有、保护和包访问属性):
java 复制代码
try {
    Class<?> clazz = Class.forName("com.reflectBean.User");
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        // 对每个 field 进行操作,比如设置可访问性
        field.setAccessible(true);
    }
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

7. 常用的Field类的方法

在Java中,Field类提供了许多方法,用于获取和操作类的字段(成员变量)。以下是一些常用的方法:

  1. 获取字段信息的方法
  • getName():返回字段的名称。
  • getType():返回表示字段类型的 Class 对象。
  • getGenericType():返回表示字段的 Type 对象,包括泛型类型信息。
  • getModifiers():返回字段的修饰符整数编码(使用 Modifier 类中的方法可以解码)。
  1. 访问和修改字段值的方法
  • get(Object obj):从指定对象中获取该字段的值。
  • getByte(Object obj):从指定对象中获取该字段的值(byte类型)。
  • set(Object obj, Object value):将指定对象的该字段设置为新的值。
  1. 访问控制
  • setAccessible(boolean flag):设置字段的可访问性。即使字段是私有的,通过设置可访问性也可以访问它。

8. 获取Method的四种方式

在Java中,可以通过反射机制获取类的方法(Method)对象。以下是几种常用的方法获取方式:

  1. 通过方法名和参数类型获取公共方法
java 复制代码
try {
            // 获取类的Class对象
            Class<?> clazz = Class.forName("com.reflectBean.User");
            // 通过方法名和参数类型获取公共方法
            Method method = clazz.getMethod("publicMethod", String.class);
            // 调用方法
            method.invoke(null, "参数");

        } catch (Exception e) {
            e.printStackTrace();
        }
  1. 获取所有公共方法
java 复制代码
try {
            // 获取类的Class对象
            Class<?> clazz = Class.forName("com.reflectBean.User");

            // 获取所有公共方法
            Method[] methods = clazz.getMethods();

            // 遍历所有方法
            for (Method method : methods) {
                // 获取方法名
                String methodName = method.getName();
                // 判断方法名是否为getName
                if (methodName.equals("getName")) {
                    // 执行getName方法
                    Object result = method.invoke(user);
                    System.out.println(result);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
  1. 通过方法名和参数类型获取任意方法(包括私有、保护和默认访问权限的方法)
java 复制代码
try {
            // 获取类的Class对象
            Class<?> clazz = Class.forName("com.reflectBean.User");

            // 通过方法名和参数类型获取任意方法
            Method method = clazz.getDeclaredMethod("privateMethod", int.class);
             // 调用方法
            method.invoke(new com.reflectBean.User(), 100);

            // 允许访问私有方法
            method.setAccessible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
  1. 获取所有的任意方法(包括私有、保护和默认访问权限的方法)
java 复制代码
try {
            // 获取类的Class对象
            Class<?> clazz = Class.forName("com.example.MyClass");

            // 获取所有任意方法
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                // 对每个 method 进行操作
                // ...
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

9. 常用的Method的方法

Method类在Java反射中提供了许多方法,这些方法可以用于获取方法的各类信息、调用方法以及操作注解等。以下是一些常用的Method类方法:

  1. 获取方法信息的方法
  • String getName():获取方法的名称。
  • Class<?> getDeclaringClass():获取声明此方法的类或接口的Class对象。
  • Class<?> getReturnType():获取方法的返回类型。
  • Type getGenericReturnType():获取方法的泛型返回类型。
  • Class<?>[] getParameterTypes():获取方法的参数类型数组。
  • Type[] getGenericParameterTypes():获取方法的泛型参数类型数组。
  • int getParameterCount():获取方法的参数数量。
  • Class<?>[] getExceptionTypes():获取方法声明的异常类型数组。
  • Type[] getGenericExceptionTypes():获取方法声明的泛型异常类型数组。
  • int getModifiers():获取方法的修饰符,用于确定方法是public、private、protected、static等。
  • Annotation[][] getParameterAnnotations():获取方法的参数上的注解。
  • String toGenericString():返回描述方法的字符串,包括泛型信息。
  1. 调用方法
  • Object invoke(Object obj, Object... args):调用此Method对象表示的底层方法。
  1. 注解相关的方法
  • <T extends Annotation> T getAnnotation(Class<T> annotationClass):获取指定类型的注解。
  • Annotation[] getAnnotations():获取此方法上存在的所有注解。
  • Annotation[] getDeclaredAnnotations():获取直接存在于此方法上的所有注解。
  • <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass):获取直接存在于此方法上的指定类型的注解。
  • <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass):获取直接存在于此方法上的指定类型的所有注解。
  • <T extends Annotation> T getAnnotationsByType(Class<T> annotationClass):获取此方法上存在的指定类型的所有注解。
  • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判断此方法上是否存在指定类型的注解。
  1. 设置和获取访问控制
  • void setAccessible(boolean flag):设置方法的可访问性。如果是 true,则强制使能此方法,即使它是私有的。
  • boolean isAccessible():判断此方法是否可以通过反射执行。
  1. 其他常用方法
  • boolean isSynthetic():判断此方法是否是编译器生成的合成方法。
  • boolean isVarArgs():判断此方法是否接受可变数量的参数。

10. 实践出真知

好了,扯了那么久反射的概念和基础使用,接下来,结合反射的内容,我们来简单演示通过反射去实现一些基本业务。

10.1 如何通过反射获取和设置对象私有字段的值?

先捋清楚思路,获取和设置对象私有字段的值,大致可以分为四个步骤实现:

  1. 获取对象的 Class 对象。

  2. 通过反射获取私有字段 Field 对象。

  3. 设置字段的可访问性为 true,以绕过 Java 语言访问检查。

  4. 通过反射获取和设置字段的值。

那么接下来演示下代码的实现流程:

假设有一个包含私有字段的类:AccountBalance.class

java 复制代码
public class AccountBalance {

    // 设置初始值为100
    private BigDecimal balance = new BigDecimal(100);

    // 默认构造函数
    public AccountBalance() {
    }

    // 打印字段值的方法
    public void printField() {
        System.out.println("当前的账户余额为: " + balance);
    }
}

接下来,我们来演示下怎么通过反射获取账户余额,然后变更私有字段:

java 复制代码
public class Test {

    public static void main(String[] args) {
        try {
            // 创建 AccountBalance 对象实例
            AccountBalance account = new AccountBalance();

            // 获取 AccountBalance 的 Class 对象
            Class<?> clazz = account.getClass();

            // 获取私有字段
            Field privateField = clazz.getDeclaredField("balance");

            // 设置字段的可访问性为 true
            privateField.setAccessible(true);

            // 获取私有字段的值
            BigDecimal initBlance = (BigDecimal) privateField.get(account);
            System.out.println("最初的账号余额为: " + initBlance);

            // 设置私有字段的值
            privateField.set(account, new BigDecimal(50));

            // 验证字段值已经更新
            initBlance = (BigDecimal) privateField.get(account);
            System.out.println("本次扣除的账户余额为: " + initBlance);

            // 使用类的方法验证字段被正确修改
            account.printField();
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

控制台输出:

10.2 如何用反射机制创建对象呢?

依旧是先整理下步骤,使用反射机制创建对象,大致可分为3个步骤:

  1. 获取类的Class对象。

  2. 通过Class对象获取构造方法(构造器)。注意,这里虽然可以通过Class对象的newInstance方法简单地创建一个类的实例,但是这种方式只能调用无参构造函数,故该实例不给予演示。

  3. 使用构造方法创建类的实例。

还是假设我们有一个包含无参和有参数构造函数的类AccountBalance.class

java 复制代码
public class AccountBalance {
    // 设置初始值为100
    private BigDecimal balance = new BigDecimal(100);

    // 默认构造函数
    public AccountBalance() {
        System.out.println("调用无参构造函数");
    }

    // 有参构造函数
    public AccountBalance(BigDecimal initialBalance) {
        this.balance = initialBalance;
        System.out.println("调用带参数的构造函数:初始化余额为 " + initialBalance);
    }

    // 打印字段值的方法
    public void printField() {
        System.out.println("当前的账户余额为: " + balance);
    }
}

使用Constructor.newInstance()创建对象并访问其方法

java 复制代码
public void Test {

     public static void main(String[] args) {
        try {
            // 获取 AccountBalance 的 Class 对象
            Class<?> clazz = Class.forName("com.example.AccountBalance");

            // 获取有参构造函数
            Constructor<?> constructor = clazz.getConstructor(BigDecimal.class);

            // 使用构造函数创建实例并传递参数
            Object obj = constructor.newInstance(new BigDecimal(200));

            // 验证对象创建成功,通过调用方法
            Method printMethod = clazz.getMethod("printField");
            printMethod.invoke(obj);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

10.3 如何反编译一个类的构造方法?

反编译一个类的构造方法,大致可以分为四个步骤实现:

  1. 获取需要被反编译的类的Class对象

  2. 获取类所有声明的构造方法

  3. 循环遍历所有的构造方法

  4. 获取每个构造方法的修饰符、参数类型并格式化输出

我们仍然以AccountBalance类为例进行说明。

java 复制代码
public class AccountBalance {
    // 设置初始值为100
    private BigDecimal balance = new BigDecimal(100);

    // 默认构造函数
    public AccountBalance() {
        System.out.println("调用无参构造函数");
    }

    // 有参构造函数
    public AccountBalance(BigDecimal initialBalance) {
        this.balance = initialBalance;
        System.out.println("调用带参数的构造函数:初始化余额为 " + initialBalance);
    }

    // 打印字段值的方法
    public void printField() {
        System.out.println("当前的账户余额为: " + balance);
    }
}

进行反编译构造方法流程:

java 复制代码
public void Test {

    public static void main(String[] args) {
        try {
            // 获取 AccountBalance 的 Class 对象
            Class<?> clazz = Class.forName("com.example.AccountBalance");

            // 获取所有声明的构造方法(包括私有构造方法)
            Constructor<?>[] constructors = clazz.getDeclaredConstructors();

            for (Constructor<?> constructor : constructors) {
                // 获取构造方法的修饰符
                String modifiers = Modifier.toString(constructor.getModifiers());

                // 获取构造方法的参数类型
                Class<?>[] parameterTypes = constructor.getParameterTypes();
                StringBuilder params = new StringBuilder();
                for (int i = 0; i < parameterTypes.length; i++) {
                    params.append(parameterTypes[i].getSimpleName());
                    if (i < parameterTypes.length - 1) {
                        params.append(", ");
                    }
                }

                // 打印构造方法的详细信息
                System.out.println(modifiers + " " + clazz.getSimpleName() + "(" + params + ") { }");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
}

10.4 给定一个类,如何获取这个类的基类以及实现的接口呢?

这种问题的思路,需要考虑两点:

  1. 怎么获取该类的父类:这点可以使用Class提供的getSuperClass()方法来获取。

  2. 怎么获取类的接口:这点可以使用Class提供的getInterfaces()方法获取实现的接口。

假设定义了以下类和接口:ChildClass.class 和 Parent.class、Car.java。

java 复制代码
public class ChildClass extends ParentClass implements Car {

}
java 复制代码
public class ParentClass {

}
java 复制代码
public interface Car {
}

获取一个类的父类和实现的接口的具体示例:

java 复制代码
public static void main(String[] args) throws ClassNotFoundException {
        try {
            // 获取 ChildClass 的 Class 对象
            Class<?> clazz = Class.forName("com.example.ChildClass");

            // 获取父类
            Class<?> superClass = clazz.getSuperclass();
            System.out.println("父类: " + superClass.getName());

            // 获取实现的接口
            Class<?>[] interfaces = clazz.getInterfaces();
            System.out.println("实现的接口:");
            for (Class<?> iface : interfaces) {
                System.out.println(iface.getName());
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
}

11. 总结

反射机制是Java语言中一种强大的功能,它让我们能够在运行时进行一些本应只能在编译时完成的操作。这个功能的特点就是使得程序可以动态地检查和操作类、方法、字段等,从而带来了极大的灵活性。简单来说,反射机制使得开发人员能够编写更加灵活和动态的代码。举个例子,我们可以根据配置文件来动态创建对象、实现依赖注入、在运行时调用方法而不需要提前知道方法的名称。

然而,反射机制虽然给我们带来了好处,但也有一些弊端需要注意。首先,反射通常会比直接调用要慢,因为它会绕过一些编译时的优化。其次,由于反射机制允许操作私有成员,可能会破坏封装性和安全性。最后,使用反射的代码可读性降低,同时也更难以维护和调试。因此,在使用反射时需要谨慎考虑其影响。

相关推荐
wuxiguala2 分钟前
【java图形化界面编程】
java·开发语言·python
东方雴翾5 分钟前
CSS语言的游戏AI
开发语言·后端·golang
鲤鱼不懂21 分钟前
python 浅拷贝copy与深拷贝deepcopy 理解
开发语言·python
创码小奇客28 分钟前
Spring Boot 中分布式事务的奇幻漂流
java·spring boot·trae
我是大头鸟36 分钟前
ecplise 工具 没有Java EE Tools 选项
java·java-ee
猫猫头有亿点炸38 分钟前
C语言之九九乘法表
c语言·开发语言
问道飞鱼1 小时前
【Vue3知识】组件间通信的方式
开发语言·javascript·ecmascript·组件·通信
树下水月1 小时前
关于使用python 安装 flask-openapi3扩展,使用docker 环境的完整复盘
开发语言·python·flask
时雨h1 小时前
《Spring Boot+策略模式:企业级度假订单Excel导入系统的架构演进与技术实现》
开发语言·bash
IDRSolutions_CN1 小时前
开发PDF时,如何比较 PDF 文件
java·经验分享·pdf·软件工程·团队开发