掘金 后端 ( ) • 2024-04-20 15:00

背景

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

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

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

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

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

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

测试

准备

我们构建一个实体类:

@Data
public static class TestBean {
    private String col1;
    private String col2;
    private String col3;
    private String col4;
    private String col5;
    private String col6;
}

编写一个耗时计算方法:

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次,进行测试:

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);
}

结果如下:

image.png

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

读数测试

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

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) {
        }
    });
}

运行结果如下:

image.png 好吧,差距更大。

硬编码setter/getter测试

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

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();
        }
    });
}

运行结果如下:

image.png

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

结果

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

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