java反射速度探寻-Method/Field读写谁快?

背景

因为工作涉及基础组件的维护,经常要写一些工具,少不了的要用到反射,因此,特意针对反射常用到的 Field 和Method,水一篇文章来测试一下使用它们读取数据的速度。

用这个问题询问一下GPT,得到如下答案:

在Java中,使用Method.invoke来调用方法通常会比使用Field.set和Field.get来读写字段慢。这是因为Method.invoke涉及到方法调用的动态解析和参数传递,还可能涉及到方法的执行逻辑,这些都增加了额外的开销。

相比之下,Field.set和Field.get直接访问类的字段,它们不需要进行方法的动态解析,也不涉及方法的执行。因此,从性能角度来看,使用Field.set和Field.get来读写字段通常会比使用Method.invoke来调用方法更高效。

然而,需要注意的是,使用反射(无论是Method还是Field)都会带来一定的性能开销,特别是在频繁访问的情况下。因此,在性能敏感的应用中,应该尽量避免使用反射,或者至少尽量减少反射的使用次数。如果可能的话,使用直接的字段访问或方法调用通常是更高效的选择。

答案已经知道了,但我觉得还是要去实验一下,看看 Method 究竟慢了多少。

测试

准备

我们构建一个实体类:

java 复制代码
@Data
public static class TestBean {
    private String col1;
    private String col2;
    private String col3;
    private String col4;
    private String col5;
    private String col6;
}

编写一个耗时计算方法:

java 复制代码
interface NoArgFunc {
    void call();
}

private static void duration(String msg, NoArgFunc func) {
    System.out.println(msg);
    long start = System.currentTimeMillis();
    func.call();
    System.out.printf("耗时: %d ms\n", System.currentTimeMillis() - start);
}

写数测试

我们为TestBean的6个字段写入100w次,进行测试:

java 复制代码
private static void doReflectionTest() {
    Field[] fields = TestBean.class.getDeclaredFields();
    for (Field field : fields) {
        field.setAccessible(true);
    }
    Method[] setters = Arrays.stream(TestBean.class.getMethods())
            .filter(method -> method.getName().startsWith("set"))
            .toList().toArray(new Method[0]);
    int cnt = 100 * 10000;
    TestBean bean = new TestBean();
    duration("测试 Field: ", () -> {
        try {
            for (int i = 0; i < cnt; i++) {
                for (Field field : fields) {
                    field.set(bean, "123");
                }
            }
        } catch (Exception ignored) {
        }
    });
    System.out.println("bean: " + bean);
    System.out.println("==========================================");
    duration("测试 Method: ", () -> {
        try {
            for (int i = 0; i < cnt; i++) {
                for (Method setter : setters) {
                    setter.invoke(bean, "abc");
                }
            }
        } catch (Exception ignored) {
        }
    });
    System.out.println("bean: " + bean);
}

结果如下:

可见,进行100w*6次调用,Field的速度接近Method的3倍!那确实差距很大。

读数测试

添加代码,同样读取TestBean的6个字段100w次,进行测试:

java 复制代码
private static void doReflectionTest() {
    Field[] fields = TestBean.class.getDeclaredFields();
    for (Field field : fields) {
        field.setAccessible(true);
    }
    Method[] setters = Arrays.stream(TestBean.class.getMethods())
            .filter(method -> method.getName().startsWith("set"))
            .toList().toArray(new Method[0]);
    Method[] getters = Arrays.stream(TestBean.class.getMethods())
            .filter(method -> method.getName().startsWith("get"))
            .toList().toArray(new Method[0]);
    int cnt = 100 * 10000;
    TestBean bean = new TestBean();
    duration("测试 Field: ", () -> {
        try {
            for (int i = 0; i < cnt; i++) {
                for (Field field : fields) {
                    field.set(bean, "123");
                }
            }
        } catch (Exception ignored) {
        }
    });
    System.out.println("bean: " + bean);
    System.out.println("==========================================");
    duration("测试 Method: ", () -> {
        try {
            for (int i = 0; i < cnt; i++) {
                for (Method setter : setters) {
                    setter.invoke(bean, "abc");
                }
            }
        } catch (Exception ignored) {
        }
    });
    System.out.println("bean: " + bean);
    System.out.println("==========================================");
    duration("测试 Field getter: ", () -> {
        try {
            for (int i = 0; i < cnt; i++) {
                for (Field field : fields) {
                    field.get(bean);
                }
            }
        } catch (Exception ignored) {
        }
    });
    System.out.println("==========================================");
    duration("测试 Method getter: ", () -> {
        try {
            for (int i = 0; i < cnt; i++) {
                for (Method getter : getters) {
                    getter.invoke(bean);
                }
            }
        } catch (Exception ignored) {
        }
    });
}

运行结果如下:

好吧,差距更大。

硬编码setter/getter测试

我们按GPT推荐的,手写一堆setter、getter调用测试一下:

java 复制代码
private static void doHardCodeTest() {
    TestBean bean = new TestBean();
    int cnt = 100 * 10000;
    duration("硬编码测试 setter: ", () -> {
        for (int i = 0; i < cnt; i++) {
            bean.setCol1("abc");
            bean.setCol2("abc");
            bean.setCol3("abc");
            bean.setCol4("abc");
            bean.setCol5("abc");
            bean.setCol6("abc");
        }
    });
    duration("硬编码测试 getter: ", () -> {
        for (int i = 0; i < cnt; i++) {
            bean.getCol1();
            bean.getCol2();
            bean.getCol3();
            bean.getCol4();
            bean.getCol5();
            bean.getCol6();
        }
    });
}

运行结果如下:

耗时要比反射方式少一半,看来反射是真的耗时啊!

结果

GPT没有骗我,经过简单的测试,Field要比Method调用快3倍左右,而硬编码调用setter/getter还要比Field调用快1倍多。

所以说,BeanUtils 的 copyProperties 还是很耗时的,如果有需要用到copyProperties的,还是建议用cglib的copier或者MMapstruct,前者动态生成代码,后者编译时生成代码,效率都很不错,下次有时间研究一下。

相关推荐
caoerzhong9 小时前
JeeWMS 开源 WMS 部署避坑指南:Java 仓库管理系统的环境基线、四类根因与可复现交付
java·开发语言·开源
用户37215742613510 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java
for_ever_love__10 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram
java资料站10 小时前
五、Spring AI Alibaba · Memory · Saver(短期会话)
java·spring·microsoft
夜之眷属11 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
波加曼大王11 小时前
# vLLM不要迷信PagedAttention神话,聊聊线上藏着的内部碎片陷阱
java·架构
SL_staff11 小时前
目标健康度自检清单:开发者视角下的目标-计划-任务链路断点诊断
java·开源·github
for_ever_love__11 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结
神仙别闹11 小时前
基于C++实现的贪吃蛇游戏平台
java·c++·游戏
布吉岛的石头11 小时前
Java 程序员第 49 阶段11:Java 接 BERT:用 ONNX Runtime 做句向量推理
java·人工智能·python·深度学习·bert·transformer