当前位置: 首页 > news >正文

什么是反射以及反射机制优缺点

        java反射机制是java语言的核心特性之一,允许程序在运行时动态获取类的内部信息并操作其成员。其核心在于绕过编译时的静态检查,实现动态加载和类结构探知的能力。

反射的优点:

1、动态性与灵活性

1.1) 运行时类操作

       反射可以在运行时动态加载类、创建对象、调用方法、访问字段,无需在编译时确定具体类型。比如下面的例子

       我们先定义一个Student类,类中成员变量包括如下信息

package com.test.demo;

public class Student {

    private String name;

    private int age;

    public String address;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    private void eat(String name) {
        System.out.println(name + "在吃饭");
    }

    private void eat(String name, int age) {
        System.out.println("姓名:"name + ", 年龄:" + age + ", 正在吃饭";
    }
}

        下面是获取字节码Class对象的三种方式 

//方法一:通过全类名我们就可以获取这个类的Class对象,这个在编译阶段就可以拿到Class对象
Class<?> clazz = Class.forName("com.test.demo.Student");
//另外获取Class对象还有另外两种方法
//方法二: 直接通过类名.class来获取,这个在加载阶段可以拿到Class对象
Class<?> clazz2 = Student.class;
//方法三:先实例化Student的一个实例对象,然后通过实例对象的getClass()方法获取Class对象,这个是在运行阶段
Student stu = new Student();
Class<?> clazz3 = stu.getClass();

        下面是通过反射获取构造方法的例子,如下:

//拿到clazz之后,就可以通过clazz.getDeclaredConstructor()获取类的无参构造方法,获取构造方法有好几个方式: 
//clazz.getDeclaredConstructors()可以获取类中所有的构造方法(包括public修饰的也包括其它类型修饰的,比如protected、private等);
//clazz.getConstructors()可以获取类中所有public修饰的构造方法。
//我们还可以通过指定参数类型来获取指定的一个构造方法,比如clazz.getDeclaredConstructor(String.class)这个就是获取只有一个String类型入参的构造方法
//在拿到构造方法后,我们就可以通过构造方法获取类的一个实例,如下所示
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object student = constructor.newInstance();
//那如果我们获取到的构造方法是个private修饰的构造方法的话,我们要创建实例对象还得加一步,就是设置构造方法可以被访问,代码如下所示
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Object student = constructor.newInstance();

         下面是通过反射获取字段和修改字段的例子,如下:

//接下来就是获取类的成员变量,如下所示,这个是获取所有用public修饰的成员变量
Field[] fields = clazz.getFields();
//如果要获取全部修饰符修饰的成员变量(public、protected、private等),那么可以通过如下方法获取,如下所示
Field[] fields = clazz.getDeclaredFields();
//如果只想获取某个指定的成员变量,那么可以通过指定字段名称来获取,比如我们的Student类中有个用private修饰的name成员变量,那么可以通过如下方式获取
Field name = clazz.getDeclaredField("name");
//如果我们想要获取这个name成员变量的值,需要先实例化一个Student对象
Student student = new Student("zhangsan", 23, "河北保定");
//然后通过name.get(obj)的方式获取成员变量name的值
Object value = name.get(student);
//打印成员变量
System.out.println(value);
//如果我们运行上面打印value的动作发现报异常了,说是没有权限访问私有成员变量的值。那么我们如何获取这个成员变量的值呢,跟上面构造方法的处理一样,我们给Field对象设置访问权限即可,代码如下
 Student student = new Student("zhangsan", 23, "河北邯郸");
 name.setAccessible(true);
 Object value = name.get(student);
 System.out.println(value);

        下面是通过反射获取成员方法和调用成员方法的例子,如下:

//接下来就是获取成员方法
//获取成员方法与获取成员变量稍有不同,通过clazz.getMethods()获取的不仅包含当前类的成员方法,还包含了父类的成员方法
Method[] methods = clazz.getMethods();
//那如果我们只想要当前类的成员方法,那么使用getDeclaredMethods方法来获取
Method[] methods = clazz.getDeclaredMethods();
//如果只想获取单个成员方法,那么我们可以通过成员方法名+参数类型的方式来获取,比如我们上面Student类中有两个私有的eat方法,两个eat方法参数不同,假如我们想要获取那个只有一个String类型参数的方法,那么就可以通过如下方式获取
Method eatMethod = clazz.getDeclaredMethod("eat", String.class);
//如果想获取有两个参数的eat方法,我们可以通过如下方式获取
Method eatMethod = clazz.getDeclaredMethod("eat", String.class, int.class);
//拿到method之后,我们还可以获取方法的参数
Parameter[] parameters = eatMethod.getParameters();
//还可以获取方法抛出的异常
Class<?>[] exceptionTypes = eatMethod.getExceptionTypes();
//最重要的是方法的执行,也就是invoke
//invoke主要包含两个入参以及返回值处理。第一个参数是obj对象,也就是我们实例化出来的实例对象,第二个参数是调用方法传递的参数。对于返回值要根据方法具体的情况来定,如果有返回值那么我们可以用一个Object对象来接收返回值,如果没有返回值,则我们不用接收即可。假如我们想要给有两个参数的eat方法设置新的参数值,然后执行一下eat方法查看打印的信息(由于eat方法是私有的,因此要执行的话必须先给eatMethod设置访问权限eatMethod.setAccessible(true)才可以)这样就可以打印出我们指定参数的执行日志了
Student student = new Student("zhangsan", 23, "河北邯郸");
Method eatMethod = clazz.getDeclaredMethod("eat", String.class, int.class); eatMethod.setAccessible(true);
eatMethod.invoke(student, "lisi", 20);
1.2)支持框架和库的开发

       反射是许多框架,比如Spring、Hibernate、Junit等的核心机制,实现依赖注入、动态代理、注解解析等。

      依赖注入的核心原理:

依赖注入的核心目标:将对象的创建和依赖关系的管理从代码中解耦。

通过反射实现依赖注入的关键步骤:

1)扫描类路径:找到需要被管理的类(如标记了特定注解的类)

2)实例化对象:通过反射创建对象。

3)递归解析依赖:检查对象的字段或构造方法,自动注入依赖的其他对象。

4)管理单例:确保同一类只被实例化一次(可选)

下面用最简单的代码来手写一个依赖注入的案例

步骤一:定义注解

       Component注解上面@Target(ElementType.TYPE)的意思是它作用于接口、类、枚举、注解。而Autowired注解上面@Target(ElementType.FIELD)的意思是它作用于字段

       @Retention包含如下几种类型

1)RetentionPolicy.SOURCE 这种类型的Annotations只在源代码级别保留,当java文件编译成class文件的时候注解被遗弃。

2)RetentionPolicy.CLASS 这种类型的注解会被保留到class文件,但jvm加载class文件的时候被遗弃,这是默认的生命周期。

3)RetentionPolicy.RUNNING 这种类型的注解不仅被保留到class文件中,jvm加载class文件之后依然保留。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}

 步骤二:下面我们模拟实现一个容器

public class DIContainer {
    //存储单例对象的容器(key是类名 value是实例)
    private final Map<String, Object> singletonMap = new ConcurrentHashMap<>();

    //扫描指定包并初始化所有Component类
    public void scan(String basePackage) throws Exception {
       //通过类路径扫描获取所有类(此处简化,实际需要实现类扫描逻辑)
       Set<Class<?>> classes = findClass(basePackage);
       for (Class<?> clazz : classes) {
          if (clazz.isAnnotationPresent(Component.class)) {
              //创建实例并注入依赖
              Object instance = createInstance(clazz);
              singletonMap.put(clazz.getName(), instance);
          }
       }
    }

    //创建对象并注入依赖
    private Object createInstance(Class<?> clazz) throws Exception {
       //1.通过无参构造方法创建实例
       Object instance = clazz.getDeclaredConstructor().newInstance();
       //2.注入字段依赖
       for (Field field : clazz.getDeclaredFields()) {
           //检查字段上是否添加了@Autowired注解
           if (field.isAnnotationPresent(Autowired.class)) {
               //获取字段类型对应的实现类(此处简化,实际需处理接口和实现类映射)
               Class<?> fieldType = field.getType();
               Object dependency = getOrCreateBean(fieldType);
               //突破private限制并注入值
               field.setAccessible(true);
               field.set(instance, dependency);
           }
       }
       return instance;
    }

    //获取或创建Bean(支持单例)
    private Object getOrCreateBean(Class<?> type) throws Exception {
        //查找已存在的实例
        Object bean = singletonMap.get(type.getName());
        if (bean != null) {
            return bean;
        }
       
        //创建新实例并递归注入依赖
        bean = createInstance(type);
        singletonMap.put(type.getName(), bean);
        return bean;
    }

    //获取Bean
    public <T> getBean(Class<?> type) {
        return (T) singletonMap.get(type.getName));
    }

    //模拟类扫描(实际需使用类加载遍历包路径)
    private Set<Class<?>> findClasses(String basePackage) {
        //示例直接返回预设类型(实际需实现扫描逻辑)
        Set<Class<?>> classes = new HashSet<>();
        classes.add(UserService.class);
        classes.add(UserRepository.class);
        return classes;
    }
}

步骤三:定义被管理的类

@Component
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public void saveUser(String user) {
        userRepository.save(user);
    }
}

@Component
public class UserRepository {
    public void save(String user) {
        System.out.println("保存用户:" + user);
    }
}

步骤四:使用容器

public class Test1 {
    public static void main(String[] args) throws Exception {
        DIContainer container = new DIContainer();
        container.scan("com.example");

        UserService userService = container.getBean(UserService.class);
        userService.saveUser("zhangsan");
    }
}

        关键点解析:

1)类扫描与实例化

        通过反射查找所有标记了@Component的类,并递归创建他们的实例。

2)依赖注入

        检查@Autowired注解的字段,动态注入已创建的依赖对象。

3)单例管理

        使用Map缓存实例,确保每个类只有一个实例。

4)简化假设

        假设@Autowired字段的类型有且仅有一个实现类。

        实际框架(如Spring)会处理接口与实现类的映射、构造方法注入、循环依赖等问题。

        反射在依赖注入中的作用

1)动态创建对象

        通过clazz.newInstance()或构造方法反射创建对象。

2)访问私有字段

        通过field.setAccessible(true)突破访问权限,注入依赖。

3)递归解析依赖

        检查字段类型,递归创建并注入依赖的Bean

2、突破访问权限

        反射可以绕过修饰符比如private、protected的限制,强制访问或修改类的私有属性和方法。

        反射是实现动态代理的基础,支持面向切面编程(AOP)。

3、通用工具开发

        3.1) 通用序列化和反序列化

        反射可以用于实现通用的JSON、XML序列化工具,如Jackson、Gson

        3.2) 调试和监控

        通过反射获取类的元数据,用于调试工具或性能监控。

反射的缺点如下:

1、性能开销

        运行效率低:反射操作需要JVM在运行时动态解析类信息,涉及方法调用、字段访问的权限检查等,性能显著低于直接代码调用。

        JIT优化受限:反射调用的代码难以被JIT编译器优化,尤其是在高频调用时可能成为性能瓶颈。

2、安全隐患

        破坏封装性:反射可以强制访问私有成员,破坏类的封装性,可能导致数据不一致或安全漏洞。

        权限管理绕过:通过反射调用setAccessible(true)可以绕过安全管理器的检查(SecurityManager),威胁系统安全。

3、代码可维护性差

        可读性差:反射代码通常冗长且难以理解,IDE难以提供代码提示和重构支持。

        编译时检查缺失:反射调用的方法或字段在编译时无法检查是否存在或类型是否匹配,错误只能在运行时暴露。

4、兼容性问题

        版本敏感性:反射代码依赖于类的内部(如方法名、参数列表、字段名),如果类发生变更(如方法重命名),反射代码会直接失效。

5、反射破坏单例

       本来我们通过双重校验模式获取单例对象只会拿到一个对象实例,但是通过反射的话,就可以调用私有方法,然后通过私有方法创建新的实例对象,这样就破坏了单例模式。

相关文章:

  • UR5e机器人位姿
  • 手机录视频风噪太大?华为Pura X“AI降风噪“太硬核了
  • ISSN号是什么?连续出版物标识的应用与生成
  • 算法 | 优化算法比较
  • 面向医药仓储场景下的药品分拣控制策略方法 研究(大纲)
  • SEARCH-R1:大型语言模型的多轮搜索推理革命
  • 安当KADP应用加密组件:高性能Java数据加密和脱敏解决方案
  • 【深度学习新浪潮】AI ISP技术与手机厂商演进历史
  • prometheus 添加alertmanager添加dingtalk机器人告警
  • 魔方AI(mofangai.net)是一个一站式人工智能资源聚合平台,致力于为用户精选国内外优质的人工智能工具
  • 模板计算(Stencil Computation)简介
  • Flink实战教程从入门到精通(基础篇)(三)Flink集群部署
  • 软考-软件设计师-面向对象
  • CSS 中@media查询的工作原理,如何利用它实现不同设备的样式适配
  • HarmonyOS Failure[MSG_ERR_INSTALL_GRANT_REQUEST_PERMISSIONS_FAILED]报错权限自查
  • ripro 主题激活 问题写入授权Token失败,可能无文件写入权限
  • 场外个股期权是什么?场外个股期权还能做吗?
  • 四.ffmpeg对yuv数据进行h264编码
  • 一道原创OI题(普及-)——ZCS的随机游走的数据生成器
  • 飞机燃油系统故障频发?数字仿真带来全新解决方案
  • 洛杉矶奥组委确认2028年奥运会和残奥会开闭幕式场地
  • 李云泽:房地产“白名单”贷款审批通过金额增至6.7万亿元
  • 玉渊谭天丨是自保还是自残?八个恶果透视美国征收100%电影关税
  • 中国海警局回应日本民用飞机侵闯我钓鱼岛领空:依法警告驱离
  • 成为中国骑手“孵化器”,环球马术冠军赛是最好的历练舞台
  • 徐丹任武汉大学药学院院长:研究领域在国际上处领跑地位