Java的幾種建立例項方法的效能對比

Supalle發表於2019-07-26

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

 

相關文章