近來打算自己封裝一個比較方便讀寫的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而不需要改變工具類的程式碼。