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

背景

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

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

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

相比之下,Field.setField.get直接访问类的字段,它们不需要进行方法的动态解析,也不涉及方法的执行。因此,从性能角度来看,使用Field.setField.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的,还是建议用cglibcopier或者MMapstruct,前者动态生成代码,后者编译时生成代码,效率都很不错,下次有时间研究一下。

相关推荐
刘小八17 分钟前
Spring AI Tool Calling 生产化:参数校验、权限控制与超时隔离
java·人工智能·spring
奶糖 肥晨24 分钟前
一次Spring Boot编译报错排查:三元运算符与包装类型的“隐形陷阱”
java·spring boot·后端
谢栋_27 分钟前
设计模式从入门到精通之(七)责任链模式
java·设计模式·责任链模式
愚公移码29 分钟前
蓝凌EKP18产品:核心执行流程
java·流程引擎
VortMall32 分钟前
全维度打磨细节体验,赋能商城稳定有序运营|VortMall 微服务商城 v1.3.11 版本发布
java·微服务·云原生·架构·商城系统·开源商城·vortmall
Tirzano35 分钟前
java 精简使用ffmpeg
java·开发语言·ffmpeg
唐青枫40 分钟前
Java Jersey 实战指南:用 JAX-RS 注解写清晰的 REST API
java
一只小小Java2 小时前
Naocs本地部署&安装3.2.3+Spring boot 3.2.0
java·spring boot·后端·nacos
JL152 小时前
Spring Boot vs Gin:Java 人搭第一个 Go REST API
java·spring boot·gin
神明不懂浪漫2 小时前
【第七章】Java中的常用类
java·开发语言·前端·经验分享·笔记