什么是反射?
在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用这个对象的所有属性和方法,这种动态获取信息和调用对象方法的功能称为 Java 反射机制。
反射有什么用?
比如,在代码运行前,我们不确定将来会使用哪一种数据结构,只有在程序运行时才决定使用哪一个数据类,而反射可以在程序运行过程中动态的获取类信息和调用类方法。那么通过反射来构造类实例的话,就不需要因为需求变更而更改源码了。
反射的用法
- 在运行时获取一个类的 Class 对象
- 在运行时构造一个类的实例化对象
- 在运行时获取一个类的所有信息:变量、方法、构造器、注解
反射的流程
- 先将 java 文件编译成 .class 文件
- 然后将 class 文件加载到JVM虚拟机中,类加载器对它进行加载
- 然后这个class文件会对外提供一个类对象,我们就可以通过这个类对象来获取类的成员变量和方法了。
获取class对象的三种方法
- Class.forName(className);通过类的全限定名获取该类的 Class 对象
- 类名.class;这种获取方式只有在编译前已经声明了该类的类型才能获取到 Class 对象
- 实例.getClass();通过实例化对象获取该实例的 Class 对象
反射应用场景
- Spring实例化对象:在 Spring 中,经常会编写一个上下文配置文件applicationContext.xml,里面是关于bean的配置,通过ClassPathXmlApplicationContext加载该配置文件,程序启动时,Spring 会将该配置文件中的所有bean都实例化,放入 IOC 容器中,IOC 容器本质上就是一个工厂,通过该工厂传入 标签的id属性获取到对应的实例。Spring 实例化对象的过程经过简化后,可以理解为反射实例化对象的步骤:也就是首先获取Class对象的构造器,接着通过构造器调用 newInstance() 方法实例化对象。
- 反射 + 抽象工厂模式:传统的工厂模式,如果需要生产新的子类,需要修改工厂类,在工厂类中增加新的分支;利用反射和工厂模式相结合,在运行时通过参数传入不同子类的全限定名获取到不同的 Class 对象,然后调用 newInstance() 方法返回不同的子类,这样在产生新的子类时,工厂类不用修改任何东西,可以专注于子类的实现,当子类确定下来时,工厂也就可以生产该子类了。(className 可以指定为 java.util.HashMap,或者 java.util.TreeMap 等等,根据业务场景来定)
- 使用 JDBC 连接数据库:在指定连接数据库的驱动类时使用反射来加载驱动类,这样我们就不需要修改源码,仅加载配置文件就可以完成驱动类的替换。(使用 Class.forName() 通过反射加载数据库的驱动程序)
反射的优缺点
- 优点:增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象
- 缺点:
- 破坏类的封装性:可以强制访问 private 修饰的信息,这违反了面向对象的封装特性。因为被 private 修饰意味着不想对外暴露,只允许本类访问,而setAccessable(true)可以无视访问修饰符的限制,外界可以强制访问。
- 性能损耗:在直接 new 对象并调用对象方法和访问属性时,编译器会在编译期提前检查可访问性,如果尝试进行不正确的访问,IDE会提前提示错误,例如参数传递类型不匹配,非法访问 private 属性和方法。而在利用反射操作对象时,编译器无法提前得知对象的类型,访问是否合法,参数传递类型是否匹配。只有在程序运行、调用反射的代码时才会从头开始检查、调用,JVM也无法对反射的代码进行优化。但反射的慢,需要同时调用上100W次才可能体现出来,在几次、几十次的调用,并不能体现反射的性能低下,在单次调用反射的过程中,性能损耗可以忽略不计。