近来打算自己封装一个比较方便读写的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<SyntaxPKTest.Man> 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<SyntaxPKTest.Man>() { 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<SyntaxPKTest.Man>) () -> 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>) 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而不需要改变工具类的代码。