Java基石--反射让你直捣黄龙

前言

前两篇分别讲解了Class和ClassLoader,正因为内存里存在着一份类的描述文件(Class类对象),我们可以依托这个类对象对程序做一些特殊的处理,而反射则是它提供的能力。

通过本篇文章,你将了解到:

  1. Java类和对象关系
  2. 什么是反射?
  3. 反射提供了哪些能力?
  4. 反射效率低吗?慢在哪?
  5. 反射的应用场景

1. JAVA类和对象关系

如上图,简述为如下几个步骤:

  1. 我们在.java文件里编写源码
  2. 编译后变成.class
  3. 当启动应用后,需要加载对应的类时,ClassLoader将.class加载到内存,并生成Class对象(同一个ClassLoader加载,全局唯一一份)
  4. Class对象拥有属性、方法的描述,一个Class可以对应无数个对象(理论可以new出无限个)

定义一个Fish类:

java 复制代码
public class Fish {
    static {
        System.out.println("fish class init");
    }
​
    String name;
​
    public Fish(String name) {
        this.name = name;
    }
​
    public void eat()
    {
        System.out.println("name is:"+name);
    }
}

引用该类:

java 复制代码
public class Main {
    public static void main(String[] args){
        Fish fish1 = new Fish("fish1");
        fish1.eat();
        Fish fish2 = new Fish("fish2");
        fish2.eat();
    }
}

打印如下:

当第一次创建对象:new Fish("fish1")时,会触发ClassLoader加载,生成Class对象存放在内存里。

当第二次创建对象:new Fish("fish2")时,不再触发ClassLoader加载。

2. 什么是反射?

new关键字

没有对象,自力更生,靠自己双手------new 出来。

这是正常的思考逻辑,new关键字创建的对象好处在于:

  1. 编译时确定类型
  2. 效率比反射高
  3. 编译时类型安全检测
  4. JIT代码优化

因此,绝大部分场景下,我们都是使用new关键字创建对象。

然而这是理想状况,总有事与愿违的时候。

ps:有些文章称new方式是正射,但实际上官方文档并没有明确这一说法。个人猜测是反射出来后,为了区分反射,才有的这定义;你可以称new方式是普通创建对象方式,也可以是正射,怎样好理解就怎样来。

反射访问私有属性/方法

项目里引用了第三方sdk,sdk对外暴露的类和方法如下:

java 复制代码
package com.fish.reflection;
public class ThirdSDK {
    public void initSDK() {
        System.out.println("initSDK");
    }
​
    private void changeStatus() {
        System.out.println("changeStatus");
    }
}

项目里引用:

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        ThirdSDK thirdSDK = new ThirdSDK();
        thirdSDK.initSDK();
        //编译报错,没法访问
        thirdSDK.changeStatus();
    }
}

因为ThirdSDK里的changeStatus是私有方法,外部是没法直接引用的,但我们又想要这个方法,该怎么办呢?

两种方式:

  1. 让三方库提供者改为public修饰(这不太可能,即使可能周期也比较长)2. 使用反射。

使用反射,只需要添加三行代码:

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        ThirdSDK thirdSDK = new ThirdSDK();
        thirdSDK.initSDK();
​
        //找到方法
        Method method = thirdSDK.getClass().getDeclaredMethod("changeStatus");
        //设置访问权限
        method.setAccessible(true);
        //调用方法
        method.invoke(thirdSDK);
    }
}

可以看出,在编译期,编译器并不知道你要调用changeStatus()方法,JVM只有在运行时才知道要调用changeStatus()方法。

当然,我们也可以通过反射获取/修改私有属性的值。

反射解耦

有时候SDK里的类并不是public修饰的,如下:

java 复制代码
package com.fish.reflection;
class Inner {
    void eat() {
        System.out.println("eat inner");
    }
}

想要访问对象方法eat(),第一步得拿到Inner对象,第二步才能调用eat()方法。

使用反射,过程如下:

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        //通过全限定类名加载Class
        Class<?> classInner = Class.forName("com.fish.reflection.Inner");
        //获取构造器
        Constructor<?> constructor = classInner.getDeclaredConstructor();
        constructor.setAccessible(true);
        //调用构造器创建对象
        Object object = constructor.newInstance();
        //有了对象剩下的就是访问私有方法
        Method method = classInner.getDeclaredMethod("eat");
        method.setAccessible(true);
        method.invoke(object);
    }
}

如上,先通过反射创建对象,再通过反射访问私有方法。

可以看出,整个过程都没有涉及到Inner这个类的引用,而是动态创建对象,调用方法。

此种方式广泛用在各种框架,模块之间联系纽带是字符串(类名),没有编译期直接的类型依赖,主要目的用于解耦。

反射定义

反射(Reflection) 是Java语言的一个特性,它允许程序在运行时检查和操作类、接口、字段和方法的信息,而不需要在编译时知道它们的名称。

核心点在于:

运行时检查:在程序执行期间检查类和对象的结构

动态操作:创建对象、调用方法、访问字段等操作

无需编译时信息:不需要在编译时知道具体要操作的类、方法或字段名称

下面类比的例子可能会对反射的理解有些许帮助:

一个景区(类)里有多个景点(属性),同时也会举办一些活动(方法)

游客从正常渠道购票进入景区,景区会附带游览指引,哪个位置有什么景点,什么时间有活动,都有具体的说明,这个时候游客对什么感兴趣直接按照地图过去就可以。因此游客游览的效率很高。

有时候景点在维护不开放(私有),那么游客无法去访问。

游客因为某种原因不从正常渠道购票,而是托关系(反射)进入了景区,此时他只知道有哪些景点和活动,不知道该怎么过去和不知道什么时候举行,于是他只能一个个地去找景点和活动(遍历访问属性和方法),并且还面临工作人员的检查(权限检测),整体效率偏低。

但由于他是托关系进去的,因此他能够去一些不开放的景点和活动区域(访问私有属性/方法),这就体现出了灵活性。

3. 反射提供了哪些能力?

反射的基础--CLASS

哦,不对,是Class满足了我们对类的一切幻想,来看看Class里可以拿到什么。

有如下代码:

java 复制代码
package com.fish.reflection;
​
interface IFish {
    void eat(String food);
    String swim();
}
​
class Animal {
    String name;
​
    public String getName() {
        return name;
    }
}
​
public class Fish extends Animal implements IFish {
​
    //鲤鱼
    static String category = "carp";
​
    static {
        System.out.println("fish class init");
    }
​
    public String name;
​
    int age = 0;
​
​
    public Fish() {
        this.name = "default";
    }
​
    public Fish(String name) {
        this.name = name;
    }
​
    private Fish(String name, int age) {
        this.name = name;
        this.age = age;
    }
​
    @Override
    public String getName() {
        return name;
    }
​
    public static String getCategory(long id) {
        System.out.println("getCategory:" + id);
        return category;
    }
​
    private void changeAge(int age) {
        this.age = age;
    }
​
    public void eat(String food) {
        System.out.println("fish eat " + food);
    }
​
    public String swim() {
        System.out.println("fish swim");
        return "fish swim";
    }
}

那么Fish.Class 对象里能拿到信息如下:

  1. Fish继承的父类、实现的接口
  2. Fish类的名字(简称和全称)
  3. Fish类的所有方法(公开/私有、静态/非静态)
  4. Fish类的所有属性(公开/私有、静态/非静态)
  5. Fish类的所有构造函数(公开/私有)
  6. Fish类的注解
  7. 其它的信息

拿到类里的方法/属性后,我们就可以调用方法和访问属性。

拿到类里的构造函数后,我们就可以创建对象。

因此Class是反射的基础。

那么,怎样获取一个Class对象呢?

有以下三种方式:

第一种:通过对象(object.getClass)获取

java 复制代码
        Fish fish = new Fish();
        Class<?> fishClass = fish.getClass();

此种方式需要能够访问类,并且先通过具体类new出对象。

第二种:通过类.class获取

java 复制代码
Class<?> fishClass1 = Fish.class;

此种方式需要能够访问类。

第三种方式:通过全限定类名获取

java 复制代码
        Class<?> fishClass2 = Class.forName("com.fish.reflection.Fish");

此种方式是完全解耦,只通过类名即可生成Class。

Class.forName先调用到C++层,最终由返回到Java层的loadClass()进行类加载,最终创建Class。

反射常用的API

以上面的Fish.java为例,讲解常用的API使用方式。

如上图,主要分析访问属性和调用方法,访问注解下篇重点分析。

调用构造函数

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> fishClass = Class.forName("com.fish.reflection.Fish");
​
        //创建对象
        Constructor<?> constructor = fishClass.getDeclaredConstructor();
        //只是通过反射创建了构造函数并调用,调用普通方法eat没有用反射
        Fish fish = (Fish)constructor.newInstance();
        fish.eat("bug");
​
        //访问不了Fish,用如下方式
        Object object = constructor.newInstance();
        Method eat = fishClass.getMethod("eat", String.class);
        eat.invoke(object, "bug2");
​
        //获取所有的public的构造方法
        Constructor<?>[] constructors = fishClass.getConstructors();
        for (Constructor<?> constructor1 : constructors) {
            System.out.println("getConstructors:" + constructor1);
        }
​
        //获取所有声明的构造方法
        Constructor<?>[] declaredConstructors = fishClass.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("getDeclaredConstructors:" + declaredConstructor);
        }
    }
}

打印结果:

调用普通方法

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> fishClass = Class.forName("com.fish.reflection.Fish");
​
        //创建对象
        Constructor<?> constructor = fishClass.getDeclaredConstructor();
        Object object = constructor.newInstance();
​
        //调用普通-对象成员-公有方法
        Method method = fishClass.getMethod("swim");
        String result = (String)method.invoke(object);
        System.out.println(result);
​
        //调用普通-对象成员-私有方法
        Method method1 = fishClass.getDeclaredMethod("changeAge", int.class);
        //私有方法需要设置 accessible 为 true
        method1.setAccessible(true);
        method1.invoke(object, 333);
​
        //调用普通-类成员-公有方法
        Method method2 = fishClass.getMethod("getCategory", long.class);
        String result2 = (String)method2.invoke(null, 44);
        System.out.println(result2);
    }
}

打印如下:

访问属性

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> fishClass = Class.forName("com.fish.reflection.Fish");
​
        //创建对象
        Constructor<?> constructor = fishClass.getDeclaredConstructor();
        Object object = constructor.newInstance();
​
        //调用普通-对象-公有属性
        Field field = fishClass.getField("name");
        field.set(object, "fish-set-public");
        System.out.println(field.get(object));
​
        //调用普通-对象成员-私有属性
        Field field1 = fishClass.getDeclaredField("age");
        //设置私有属性可访问
        field1.setAccessible(true);
        field1.set(object, 18);
        System.out.println(field1.get(object));
​
        //调用普通-类属性-私有属性
        Field field2 = fishClass.getDeclaredField("category");
        field2.setAccessible(true);
        field2.set(null, "fish-set-static");
        System.out.println(field2.get(null));
    }
}

打印如下:

4. 反射效率低吗?慢在哪?

你也许在不同的文章里看到不少说反射效率低的说法,那么它真的低吗?

使用如下代码进行测试:

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> fishClass = Class.forName("com.fish.reflection.Fish");
​
        long starTime = System.currentTimeMillis();
        for(int i = 0; i < 10000; i++) {
            Fish fish = new Fish();
        }
        System.out.println("普通创建对象花费:cost:" + (System.currentTimeMillis() - starTime) + "毫秒");
​
        long starTime1 = System.currentTimeMillis();
        for(int i = 0; i < 10000; i++) {
            Constructor<?> constructor = fishClass.getDeclaredConstructor();
            Object object = constructor.newInstance();
        }
        System.out.println("反射创建对象花费:cost:" + (System.currentTimeMillis() - starTime1) + "毫秒");
​
        long starTime2 = System.currentTimeMillis();
        for(int i = 0; i < 10000; i++) {
            Fish fish = new Fish();
            fish.eat("fish");
        }
        System.out.println("普通创建对象-调用方法花费:cost:" + (System.currentTimeMillis() - starTime2) + "毫秒");
​
        long starTime3 = System.currentTimeMillis();
        for(int i = 0; i < 10000; i++) {
            Constructor<?> constructor = fishClass.getDeclaredConstructor();
            Object object = constructor.newInstance();
            Method method1 = fishClass.getDeclaredMethod("changeAge", int.class);
            //私有方法需要设置 accessible 为 true
            method1.setAccessible(true);
            method1.invoke(object, 333);
        }
        System.out.println("反射创建对象-调用方法花费:cost:" + (System.currentTimeMillis() - starTime3) + "毫秒");
    }
}

普通的对象创建和反射对象创建对比,普通方法调用和反射方法调用对比。

结果如下:

明显可以看出,反射是比普通方式耗时,主要耗时在获取方法和属性时需要遍历,同时私有的方法/属性需要设置权限绕过。

但差距并不大,当然你可以继续加大循环次数测试,也许对比会更明显。

每一代的JVM可能会对反射的效率有优化,不同的应用场景对反射性能敏感度不一样,因此具体情况具体分析,你觉得反射影响了你的性能,那么可以考虑放到其它线程或者不使用它(比如Android设备上早期性能较差,反射如果在主线程执行会影响UI渲染)。

反射依旧是解耦的利器,用得巧妙将会事半功倍。

5. 反射的应用场景

1. 框架开发

依赖注入框架:如Spring框架使用反射来实现依赖注入,通过反射获取注解信息并实例化对象

ORM框架:如Hibernate、MyBatis等框架使用反射来映射数据库表与Java对象

Web框架:如Spring MVC使用反射处理控制器方法的调用

2. 配置文件解析

XML配置处理:通过反射解析XML配置文件,动态创建和配置对象

注解处理:解析类、方法、字段上的注解信息,实现各种功能

3. 动态代理

AOP实现:Spring AOP使用Java动态代理创建代理对象

RPC框架:远程方法调用中动态生成代理类

4. 序列化和反序列化

JSON处理:Jackson、Gson等库使用反射将Java对象转换为JSON格式

对象持久化:将对象状态保存到文件或数据库中

5. 其它

如果本文对你有帮助,请一键三连~

下篇将分析反射的另一个框架里的高频应用:注解,敬请期待~

相关推荐
uzong1 小时前
技术故障复盘模版
后端
GetcharZp1 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
桦说编程2 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研2 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
一只爱撸猫的程序猿3 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
excel3 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
甄超锋3 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat