背景
因为工作涉及基础组件的维护,经常要写一些工具,少不了的要用到反射,因此,特意针对反射常用到的 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
,前者动态生成代码,后者编译时生成代码,效率都很不错,下次有时间研究一下。