Java反射【详解】

297 阅读9分钟

1、反射的概念

Java反射是指在运行时动态地获取一个类的信息,包括其方法、属性、构造器等,并且可以在运行时修改类的行为(在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息)。Java反射主要是通过java.lang.reflect包下的类实现的。

通过反射的能力,可以让Java语言知识动态获取程序的信息以及动态调用方法的能力。在Java里面专门有一个叫java.lang.reflect这样一个包,来实现反射相关的一些类库,包括ConstructFieldMethodClass等这样一些类,分别用来获取类的构造方法、成员变量、方法信息和属性方法

反射使用的场景还挺多的,比如在动态代理这样一个场景里面,使用动态深层的代理类,来提升代码的复用性。在Spring框架里面有大量用到反射,比如用反射来实例化Bean对象等等。

补充:

  • 编译期:编译器把源代码翻译成机器码
  • 运行期:将可执行文件交给操作系统去执行

2、反射的实现方式(获取Class对象的几种方法)

(1)调用某个类的class属性来获取该类对应的Class对象(知道具体类的情况下可以使用)。类名.class

Class class1 = Person.class;//获取到类对象

但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化。

(2)调用某个对象的getClass()方法。对象名.getClass()

Person p =new Person();
Class class2 = p.getClass();

(3)通过包名,调用class的forName方法(最安全/性能最好)。Class.forName(“类的路径”)

Class class3 =  Class.forName("day07.Person");

(4)通过类加载器 xxxClassLoader.loadClass() 传入类路径获取

class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");

其中第3种方式是用的最多,只知道包名类名,其他啥也拿不到那种;个人认为这才是反射存在的意义。

3、new和反射创建有什么区别?

除了使用new创建对象之外,还可以用什么方法创建对象?——使用Java反射可以创建对象!

  • new:静态编译,在编译期就将模块编译进来,执行该字节码文件,所有的模块都被加载;
  • 反射:动态编译,编译期没有加载,等到模块被调用时才加载;

注:spring的ioc就用到反射机制,newInstance创建。更加的通用,并且降低耦合,避免了硬编码,只需提供一个字符串就可以动态的创建。

String className = readfromXMlConfig;//从xml 配置文件中获得字符串
Class c = Class.forName(className);
factory = (AInterface)c.newInstance();

举例: 有自行车🚲,小轿车🚗。(1)静态:上班将自行车放车里去上班,一定得带着,一个不能落下;(2)动态:将自行车放家里,交通堵塞骑自行车去,车放家里。

拓展:Java反射创建对象效率高还是通过new创建对象的效率高?

答:通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低

4、反射的优缺点

优点

  • 增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作(面试时可以先说后半句,再说前半句,似乎会流畅一些)
  • 提高代码的复用率,比如动态代理,就是用到了反射来实现
  • 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用

缺点

  • 性能问题
    • 使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。
    • 反射会涉及到动态类型的解析,所以JVM无法对这些代码进行优化,导致性能要比非反射调用更低。
  • 模糊内部逻辑
    • 程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
  • 内部暴露
    • 由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用——代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
  • 安全问题
    • 相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)

5、反射提供了哪些功能?(可以实现什么)

  • 在运行时,能够通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息
  • 在运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员
  • 在运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象

6、实现Java反射的类:

  • Class:表示正在运行的Java应用程序中的类和接口(注意: 所有获取对象的信息都需要Class类来实现)
  • Field:提供有关类和接口的属性信息,以及对它的动态访问权限。
  • Constructor:提供关于类的单个构造方法的信息以及它的访问权限
  • Method:提供类或接口中某个方法的信息

7、反射 API

反射 API 用来生成 JVM 中的类、接口或者对象的信息。

  1. Class 类:反射的核心类,可以获取类的属性,方法等信息。
  2. Field 类:Java.lang.reflect 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  3. Method 类:Java.lang.reflect 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
  4. Constructor 类:Java.lang.reflect 包中的类,表示类的构造方法。

8、反射使用步骤(获取Class对象、调用对象方法)

  1. 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。
  2. 调用 Class 类中的方法,既就是反射的使用阶段。
  3. 使用反射 API 来操作这些信息。

9、利用反射动态创建对象实例

Class 对象的 newInstance()

  1. 使用 Class 对象的 newInstance()方法来创建该 Class 对象对应类的实例,但是这种方法要求该 Class 对象对应的类有默认的空构造器。

    调用 Constructor 对象的 newInstance()

  2. newInstance()方法来创建 Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例。

10、为什么要引入反射?

  1. 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
  2. 反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
  3. 测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率

11、反射的应用场景

像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。

但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。

这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射

JDK实现动态代理

下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法。

public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理类中的真实对象
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }


    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("after method " + method.getName());
        return result;
    }
}

注解/XML配置

另外,像 Java 中的一大利器 注解 的实现也用到了反射。(XML配置也用到了反射)

为什么你使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?

这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。

//获取类名 
String className = "com.example.demo.User"; 
   try { 
   //使用反射机制加载类 
       Class clazz = Class.forName(className); 
       //创建对象实例 
       Object obj = clazz.newInstance(); 
   } catch (Exception e) { 
       e.printStackTrace(); 
   } 

使用 JDBC 连接数据库

在 JDBC 的操作中,如果要想进行数据库的连接,则需要先通过反射机制加载数据库的驱动程序。步骤如下:

  • 通过 Class.forName() 加载数据库的驱动程序 (通过反射加载)
  • 通过 DriverManager 类连接数据库,参数包含数据库的连接地址、用户名、密码
  • 通过 Connection 接口接收连接
  • 关闭连接
public static void main(String[] args) throws Exception {  
        Connection con = null; // 数据库的连接对象  
        // 1. 通过反射加载驱动程序
        Class.forName("com.mysql.jdbc.Driver"); 
        // 2. 连接数据库  
        con = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/test","root","root"); 
        // 3. 关闭数据库连接
        con.close(); 
}

Spring 框架

(实际上是因为使用了动态代理,所以才和反射机制有关)

反射机制是 Java 框架设计的灵魂,框架的内部都已经封装好了,我们自己基本用不着写。典型的除了Hibernate 之外,还有 Spring 也用到了很多反射机制,最典型的就是 Spring 通过 xml 配置文件装载 Bean(创建对象),也就是 Spring 的 IoC,过程如下:

  • 加载配置文件,获取 Spring 容器
  • 使用反射机制,根据传入的字符串获得某个类的 Class 实例
// 获取 Spring 的 IoC 容器,并根据 id 获取对象
public static void main(String[] args) {
    // 1.使用 ApplicationContext 接口加载配置文件,获取 spring 容器
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
    // 2. 使用反射机制,根据这个字符串获得某个类的 Class 实例
    IAccountService aService = (IAccountService) ac.getBean("accountServiceImpl");
    System.out.println(aService);
}

另外,Spring AOP 由于使用了动态代理,所以也使用了反射机制

参考文章:请说说你对反射的了解