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. 其它

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

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

相关推荐
*愿风载尘*2 分钟前
ksql连接数据库免输入密码交互
数据库·后端
溟洵8 分钟前
Qt 窗口 工具栏QToolBar、状态栏StatusBar
开发语言·前端·数据库·c++·后端·qt
GEM的左耳返9 分钟前
Java面试实战:从基础到架构的全方位技术交锋
spring boot·微服务·云原生·java面试·技术解析·ai集成
用户25191624271113 分钟前
Canvas之图像合成
前端·javascript·canvas
每天开心13 分钟前
噜噜旅游App(4)——构建旅游智能客服模块,实现AI聊天
前端·微信小程序·前端框架
超凌14 分钟前
el-input-number出现的点击+-按钮频现不生效
前端
三小河15 分钟前
contentEditable 实现可编辑区域
前端
ppo9219 分钟前
MCP简单应用:使用SpringAI + Cline + DeepSeek实现AI创建文件并写入内容
人工智能·后端
一道雷23 分钟前
🧩 Vue Router嵌套路由新范式:无需嵌套 RouterView 的布局实践
前端·vue.js
创码小奇客27 分钟前
Talos 使用全攻略:从基础到高阶,常见问题一网打尽
java·后端·架构