Java的几种创建实例方法的性能对比
近来打算自己封装一个比较方便读写的Office Excel 工具类,前面已经写了一些,比较粗糙本就计划重构一下,刚好公司的电商APP后台原有的导出Excel实现出现了可怕的性能问题,600行的数据生成Excel工作簿居然需要50秒以上,客户端连接都被熔断了还没导出来,挺巧,那就一起解决吧。
在上一个版本里呢,我认为比较巧妙的地方在于用函数式编程的方式代替反射,很早以前了解了反射的一些底层后我就知道反射的性能很差,但一直没实际测试过各种调用场景的性能差距。
至于底层字节码、CPU指令这些我就不深究了,我还没到那个级别,那这次就来个简单的测试吧。
目标:创建Man对象。
方式:
① 直接引用 new Man();
② 使用反射
③ 使用内部类
④ 使用Lombda表达式
⑤ 使用Method Reference
在学习Java8新特性的时候,我所了解到的是Lombda表达式是内部类的一种简化书写方式,也就是语法糖,但两者间在运行时居然有比较明显的性能差距,让我不得不怀疑它底层到底是啥东西,时间精力有限先记着,有必要的时候再去啃openJDK吧。
还有就是Lombda和Method Reference从表现来看,底层应该是同一个东西,但IDEA既然分开两种内部类的写法推荐,那就分开对待吧。
测试时每种方式循环调用 1 亿次,每种方式连续计算两次时间,然后对比第二次运行的结果,直接run没有采用debug运行。
贴代码:
复制代码
1 package com.supalle.test;
2
3 import lombok.AllArgsConstructor;
4 import lombok.Builder;
5 import lombok.Data;
6 import lombok.NoArgsConstructor;
7
8 import java.lang.reflect.Constructor;
9 import java.lang.reflect.InvocationTargetException;
10 import java.util.function.Supplier;
11
12 /**
13 * @描述:语法PK
14 * @作者:Supalle
15 * @时间:2019/7/26
16 */
17 public class SyntaxPKTest {
18
19
20 / 循环次数 /
21 private final static int SIZE = 100000000;
22
23 / 有类如下 /
24 @Data
25 @Builder
26 @NoArgsConstructor
27 @AllArgsConstructor
28 private static class Man {
29 private String name;
30 private int age;
31 }
32
33
34 /**
35 * 使用 new Man();
36 *
37 * @return 运行耗时
38 */
39 public static long runWithNewConstructor() {
40 long start = System.currentTimeMillis();
41
42 for (int i = 0; i < SIZE; i++) {
43 new SyntaxPKTest.Man();
44 }
45
46 return System.currentTimeMillis() - start;
47 }
48
49 /**
50 * 使用反射
51 *
52 * @return 运行耗时
53 */
54 public static long runWithReflex() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
55 Constructor constructor = SyntaxPKTest.Man.class.getConstructor();
56 long start = System.currentTimeMillis();
57
58 for (int i = 0; i < SIZE; i++) {
59 constructor.newInstance();
60 }
61
62 return System.currentTimeMillis() - start;
63 }
64
65 /**
66 * 使用内部类调用 new Man();
67 *
68 * @return 运行耗时
69 */
70 public static long runWithSubClass() {
71 long start = System.currentTimeMillis();
72
73 for (int i = 0; i < SIZE; i++) {
74 new Supplier() {
75 @Override
76 public SyntaxPKTest.Man get() {
77 return new SyntaxPKTest.Man();
78 }
79 }.get();
80
81 }
82
83 return System.currentTimeMillis() - start;
84 }
85
86 /**
87 * 使用Lambda调用 new Man();
88 *
89 * @return 运行耗时
90 */
91 public static long runWithLambda() {
92 long start = System.currentTimeMillis();
93
94 for (int i = 0; i < SIZE; i++) {
95 ((Supplier) () -> new SyntaxPKTest.Man()).get();
96 }
97
98 return System.currentTimeMillis() - start;
99 }
100
101
102 /**
103 * 使用 MethodReference
104 *
105 * @return 运行耗时
106 */
107 public static long runWithMethodReference() {
108 long start = System.currentTimeMillis();
109
110 for (int i = 0; i < SIZE; i++) {
111 ((Supplier) SyntaxPKTest.Man::new).get();
112 }
113
114 return System.currentTimeMillis() - start;
115 }
116
117 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
118
119 // 测试前调用一下,加载Man字节码,尽量公平
120 SyntaxPKTest.Man man1 = new SyntaxPKTest.Man();
121 SyntaxPKTest.Man man2 = new SyntaxPKTest.Man("张三", 20);
122
123 System.out.println("测试环境:CPU核心数 - " + Runtime.getRuntime().availableProcessors());
124
125 System.out.println();
126
127 // 这里的话对比再次调用的时间
128 System.out.println("首次使用 new Man() 耗时:" + runWithNewConstructor());
129 System.err.println("再次使用 new Man() 耗时:" + runWithNewConstructor());
130 System.out.println("首次使用反射 耗时:" + runWithReflex());
131 System.err.println("再次使用反射 耗时:" + runWithReflex());
132 System.out.println("首次使用内部类调用 new Man() 耗时:" + runWithSubClass());
133 System.err.println("再次使用内部类调用 new Man() 耗时:" + runWithSubClass());
134 System.out.println("首次使用Lambda调用 new Man() 耗时:" + runWithLambda());
135 System.err.println("再次使用Lambda调用 new Man() 耗时:" + runWithLambda());
136 System.out.println("首次使用 MethodReference 耗时:" + runWithMethodReference());
137 System.err.println("再次使用 MethodReference 耗时:" + runWithMethodReference());
138
139
140 }
141
142 }
复制代码
运行结果:
一:
复制代码
测试环境:CPU核心数 - 8
首次使用 new Man() 耗时:5
再次使用 new Man() 耗时:3
首次使用反射 耗时:312
再次使用反射 耗时:276
首次使用内部类调用 new Man() 耗时:6
再次使用内部类调用 new Man() 耗时:3
首次使用Lambda调用 new Man() 耗时:142
再次使用Lambda调用 new Man() 耗时:100
首次使用 MethodReference 耗时:86
再次使用 MethodReference 耗时:85
复制代码
二:
复制代码
测试环境:CPU核心数 - 8
首次使用 new Man() 耗时:5
再次使用 new Man() 耗时:2
首次使用反射 耗时:326
再次使用反射 耗时:275
首次使用内部类调用 new Man() 耗时:6
再次使用内部类调用 new Man() 耗时:3
首次使用Lambda调用 new Man() 耗时:122
再次使用Lambda调用 new Man() 耗时:86
首次使用 MethodReference 耗时:102
再次使用 MethodReference 耗时:83
复制代码
三:
复制代码
测试环境:CPU核心数 - 8
首次使用 new Man() 耗时:5
再次使用 new Man() 耗时:3
首次使用反射 耗时:322
再次使用反射 耗时:288
首次使用内部类调用 new Man() 耗时:7
再次使用内部类调用 new Man() 耗时:2
首次使用Lambda调用 new Man() 耗时:128
再次使用Lambda调用 new Man() 耗时:92
首次使用 MethodReference 耗时:97
再次使用 MethodReference 耗时:81
复制代码
如果Lambda和MethodReference调换一下位置如下:
复制代码
1 System.out.println("首次使用 new Man() 耗时:" + runWithNewConstructor());
2 System.err.println("再次使用 new Man() 耗时:" + runWithNewConstructor());
3 System.out.println("首次使用反射 耗时:" + runWithReflex());
4 System.err.println("再次使用反射 耗时:" + runWithReflex());
5 System.out.println("首次使用内部类调用 new Man() 耗时:" + runWithSubClass());
6 System.err.println("再次使用内部类调用 new Man() 耗时:" + runWithSubClass());
7 System.out.println("首次使用 MethodReference 耗时:" + runWithMethodReference());
8 System.err.println("再次使用 MethodReference 耗时:" + runWithMethodReference());
9 System.out.println("首次使用Lambda调用 new Man() 耗时:" + runWithLambda());
10 System.err.println("再次使用Lambda调用 new Man() 耗时:" + runWithLambda());
复制代码
一:
复制代码
测试环境:CPU核心数 - 8
首次使用 new Man() 耗时:6
再次使用 new Man() 耗时:2
首次使用反射 耗时:351
再次使用反射 耗时:270
首次使用内部类调用 new Man() 耗时:6
再次使用内部类调用 new Man() 耗时:3
首次使用 MethodReference 耗时:128
再次使用 MethodReference 耗时:97
首次使用Lambda调用 new Man() 耗时:82
再次使用Lambda调用 new Man() 耗时:74
复制代码
二:
复制代码
测试环境:CPU核心数 - 8
首次使用 new Man() 耗时:5
再次使用 new Man() 耗时:3
首次使用反射 耗时:318
再次使用反射 耗时:297
首次使用内部类调用 new Man() 耗时:6
再次使用内部类调用 new Man() 耗时:2
首次使用 MethodReference 耗时:117
再次使用 MethodReference 耗时:100
首次使用Lambda调用 new Man() 耗时:91
再次使用Lambda调用 new Man() 耗时:79
复制代码
三:
复制代码
测试环境:CPU核心数 - 8
首次使用 new Man() 耗时:6
再次使用 new Man() 耗时:3
首次使用反射 耗时:319
再次使用反射 耗时:277
首次使用内部类调用 new Man() 耗时:8
再次使用内部类调用 new Man() 耗时:3
首次使用 MethodReference 耗时:139
再次使用 MethodReference 耗时:85
首次使用Lambda调用 new Man() 耗时:103
再次使用Lambda调用 new Man() 耗时:84
复制代码
总结:
① 如果不需要足够的灵活性,直接使用 new 来构造一个对象,效率最高的。
② 反射确确实实是垫底,当然它也给我们提供了足够全面的、灵活的类操纵能力。
③ 使用内部类的方式,效率上和直接new 非常贴近,虽然看起来代码多一些,但是足够灵活。
④ Lambda和Method Reference效率其实很贴近,又是一起在Java8推出的,底层实现应该是一样的,在效率上比起反射好很多。
上个版本中,我使用的Method Reference,下个版本还会继续使用Method Reference,因为接口方式和内部类一致,如果碰到某些对性能要求非常极致的使用场景,可以在使用时以内部类的方式替代Method Reference而不需要改变工具类的代码。