請不要再說 Java 中 final 方法比非 final 效能更好了
無繼承
有 static 修飾
static final
// 生成隨機數字和字母, public static final String getStringRandomFinal(int length) { String val = ""; Random random = new Random(); // 引數length,表示生成幾位隨機數 for (int i = 0; i < length; i++) { String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 輸出字母還是數字 if ("char".equalsIgnoreCase(charOrNum)) { // 輸出是大寫字母還是小寫字母 // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (random.nextInt(26) + 97); } else if ("num".equalsIgnoreCase(charOrNum)) { val += String.valueOf(random.nextInt(10)); } } return val; }
static 非 final
// 生成隨機數字和字母, public static String getStringRandom(int length) { String val = ""; Random random = new Random(); // 引數length,表示生成幾位隨機數 for (int i = 0; i < length; i++) { String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 輸出字母還是數字 if ("char".equalsIgnoreCase(charOrNum)) { // 輸出是大寫字母還是小寫字母 // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (random.nextInt(26) + 97); } else if ("num".equalsIgnoreCase(charOrNum)) { val += String.valueOf(random.nextInt(10)); } } return val; }
結果
這裡使用了 OpenJDK 的 JMH 基準測試工具來測試的,結果如下:
# JMH 1.4.1 (released 903 days ago, please consider updating!) # VM invoker: /srv/jdk1.8.0_92/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.agoncal.sample.jmh.Main.benchmark 中間忽略了預熱及測試過程,這裡只顯示結果 Result: 206924.113 ±(99.9%) 7746.446 ops/s [Average] Statistics: (min, avg, max) = (132107.466, 206924.113, 267265.397), stdev = 32798.937 Confidence interval (99.9%): [199177.667, 214670.559] # JMH 1.4.1 (released 903 days ago, please consider updating!) # VM invoker: /srv/jdk1.8.0_92/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal 中間忽略了預熱及測試過程,這裡只顯示結果 Result: 210111.568 ±(99.9%) 8486.176 ops/s [Average] Statistics: (min, avg, max) = (133813.368, 210111.568, 267525.228), stdev = 35931.001 Confidence interval (99.9%): [201625.392, 218597.744] # Run complete. Total time: 00:13:54 Benchmark Mode Samples Score Error Units o.a.s.j.Main.benchmark thrpt 200 206924.113 ± 7746.446 ops/s o.a.s.j.Main.benchmarkFinal thrpt 200 210111.568 ± 8486.176 ops/s
總結:你說final的效能比非final有沒有提升呢?可以說有,但幾乎可以忽略不計。如果單純地追求效能,而將所有的方法修改為 final 的話,我認為這樣子是不可取的。而且這效能的差別,遠遠也沒有網上有些人說的提升 50% 這麼恐怖(有可能他們使用的是10年前的JVM來測試的吧^_^,比如 《35+ 個 Java 程式碼效能優化總結》這篇文章。雷總:不服?我們們來跑個分!)
分析
位元組碼級別的差別
StringKit.java
StringKitFinal.java
它們在位元組碼上的差別:
[18:52:08] emacsist:target $ diff /tmp/stringkit.log /tmp/stringkit-final.log 1,5c1,5 < Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class < Last modified 2017-6-15; size 1098 bytes < MD5 checksum fe1ccdde26107e4037afc54c780f2c95 < Compiled from "StringKit.java" < public class org.agoncal.sample.jmh.StringKit --- > Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class > Last modified 2017-6-15; size 1118 bytes > MD5 checksum 410f8bf0eb723b794e4754c6eb8b9829 > Compiled from "StringKitFinal.java" > public class org.agoncal.sample.jmh.StringKitFinal 24c24 < #15 = Class #52 // org/agoncal/sample/jmh/StringKit --- > #15 = Class #52 // org/agoncal/sample/jmh/StringKitFinal 32,33c32,33 < #23 = Utf8 Lorg/agoncal/sample/jmh/StringKit; < #24 = Utf8 getStringRandom --- > #23 = Utf8 Lorg/agoncal/sample/jmh/StringKitFinal; > #24 = Utf8 getStringRandomFinal 47c47 < #38 = Utf8 StringKit.java --- > #38 = Utf8 StringKitFinal.java 61c61 < #52 = Utf8 org/agoncal/sample/jmh/StringKit --- > #52 = Utf8 org/agoncal/sample/jmh/StringKitFinal 75c75 < public org.agoncal.sample.jmh.StringKit(); --- > public org.agoncal.sample.jmh.StringKitFinal(); 87c87 < 0 5 0 this Lorg/agoncal/sample/jmh/StringKit; --- > 0 5 0 this Lorg/agoncal/sample/jmh/StringKitFinal; 89c89 < public static java.lang.String getStringRandom(int); --- > public static final java.lang.String getStringRandomFinal(int); 91c91 < flags: ACC_PUBLIC, ACC_STATIC --- > flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL 187c187 < SourceFile: "StringKit.java" --- > SourceFile: "StringKitFinal.java"
可以看到除了方法名和方法修飾符不同之外,其他的沒有什麼區別了。
在呼叫者上面的位元組碼差別
public void benchmark(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: bipush 32 2: invokestatic #2 // Method org/agoncal/sample/jmh/StringKit.getStringRandom:(I)Ljava/lang/String; 5: pop 6: return LineNumberTable: line 21: 0 line 22: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lorg/agoncal/sample/jmh/Main; RuntimeVisibleAnnotations: 0: #26() public void benchmarkFinal(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: bipush 32 2: invokestatic #3 // Method org/agoncal/sample/jmh/StringKitFinal.getStringRandomFinal:(I)Ljava/lang/String; 5: pop 6: return LineNumberTable: line 26: 0 line 27: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lorg/agoncal/sample/jmh/Main; RuntimeVisibleAnnotations: 0: #26()
可以看到,它們在呼叫者上面的位元組碼也沒有什麼區別,只是方法名不一樣之外。
對於 JVM 來說,它是隻認位元組碼的,既然位元組碼除了方法名和修飾符一樣,其他都一樣,那就可以大概推測它們的效能幾乎可以忽略不計了。因為呼叫 static final 和 static 非 final 的JVM指令是一樣。
無 static 修飾
方法體是一樣的,只是將它們刪除了 static 的修飾。
結果
# JMH version: 1.19 # VM version: JDK 1.8.0_92, VM 25.92-b14 # VM invoker: /srv/jdk1.8.0_92/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.agoncal.sample.jmh.Main.benchmark 中間忽略了預熱及測試過程,這裡只顯示結果 Result "org.agoncal.sample.jmh.Main.benchmark": 201306.770 ±(99.9%) 8184.423 ops/s [Average] (min, avg, max) = (131889.934, 201306.770, 259928.172), stdev = 34653.361 CI (99.9%): [193122.347, 209491.193] (assumes normal distribution) # JMH version: 1.19 # VM version: JDK 1.8.0_92, VM 25.92-b14 # VM invoker: /srv/jdk1.8.0_92/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal 中間忽略了預熱及測試過程,這裡只顯示結果 Result "org.agoncal.sample.jmh.Main.benchmarkFinal": 196871.022 ±(99.9%) 8595.719 ops/s [Average] (min, avg, max) = (131182.268, 196871.022, 265522.769), stdev = 36394.814 CI (99.9%): [188275.302, 205466.741] (assumes normal distribution) # Run complete. Total time: 00:13:35 Benchmark Mode Cnt Score Error Units Main.benchmark thrpt 200 201306.770 ± 8184.423 ops/s Main.benchmarkFinal thrpt 200 196871.022 ± 8595.719 ops/s
分析
位元組碼級別的差別
[19:20:17] emacsist:target $ diff /tmp/stringkit.log /tmp/stringkit-final.log 1,5c1,5 < Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class < Last modified 2017-6-15; size 1110 bytes < MD5 checksum f61144e86f7c17dc5d5f2b2d35fac36d < Compiled from "StringKit.java" < public class org.agoncal.sample.jmh.StringKit --- > Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class > Last modified 2017-6-15; size 1130 bytes > MD5 checksum 15ce17ee17fdb5f4721f0921977b1e69 > Compiled from "StringKitFinal.java" > public class org.agoncal.sample.jmh.StringKitFinal 24c24 < #15 = Class #52 // org/agoncal/sample/jmh/StringKit --- > #15 = Class #52 // org/agoncal/sample/jmh/StringKitFinal 32,33c32,33 < #23 = Utf8 Lorg/agoncal/sample/jmh/StringKit; < #24 = Utf8 getStringRandom --- > #23 = Utf8 Lorg/agoncal/sample/jmh/StringKitFinal; > #24 = Utf8 getStringRandomFinal 47c47 < #38 = Utf8 StringKit.java --- > #38 = Utf8 StringKitFinal.java 61c61 < #52 = Utf8 org/agoncal/sample/jmh/StringKit --- > #52 = Utf8 org/agoncal/sample/jmh/StringKitFinal 75c75 < public org.agoncal.sample.jmh.StringKit(); --- > public org.agoncal.sample.jmh.StringKitFinal(); 87c87 < 0 5 0 this Lorg/agoncal/sample/jmh/StringKit; --- > 0 5 0 this Lorg/agoncal/sample/jmh/StringKitFinal; 89c89 < public java.lang.String getStringRandom(int); --- > public final java.lang.String getStringRandomFinal(int); 91c91 < flags: ACC_PUBLIC --- > flags: ACC_PUBLIC, ACC_FINAL 169c169 < 0 125 0 this Lorg/agoncal/sample/jmh/StringKit; --- > 0 125 0 this Lorg/agoncal/sample/jmh/StringKitFinal; 188c188 < SourceFile: "StringKit.java" --- > SourceFile: "StringKitFinal.java"
可以看到,位元組碼上除了名字和 final 修飾符差別外,其餘的是一樣的。
在呼叫者上面的位元組碼差別
public void benchmark(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: new #2 // class org/agoncal/sample/jmh/StringKit 3: dup 4: invokespecial #3 // Method org/agoncal/sample/jmh/StringKit."<init>":()V 7: bipush 32 9: invokevirtual #4 // Method org/agoncal/sample/jmh/StringKit.getStringRandom:(I)Ljava/lang/String; 12: pop 13: return LineNumberTable: line 21: 0 line 22: 13 LocalVariableTable: Start Length Slot Name Signature 0 14 0 this Lorg/agoncal/sample/jmh/Main; RuntimeVisibleAnnotations: 0: #30() public void benchmarkFinal(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: new #5 // class org/agoncal/sample/jmh/StringKitFinal 3: dup 4: invokespecial #6 // Method org/agoncal/sample/jmh/StringKitFinal."<init>":()V 7: bipush 32 9: invokevirtual #7 // Method org/agoncal/sample/jmh/StringKitFinal.getStringRandomFinal:(I)Ljava/lang/String; 12: pop 13: return LineNumberTable: line 26: 0 line 27: 13 LocalVariableTable: Start Length Slot Name Signature 0 14 0 this Lorg/agoncal/sample/jmh/Main; RuntimeVisibleAnnotations: 0: #30()
可以看到,它們除了名字不同之外,其他的JVM指令都是一樣的。
總結
對於是否有 final 修飾的方法,對效能的影響可以忽略不計。因為它們生成的位元組碼除了 flags 標誌位是否有 final 修飾不同之外,其他所有的JVM指令,都是一樣的(對於方法本身,以及呼叫者本身的位元組碼都一樣)。對於JVM來說,它執行的就是位元組碼,如果位元組碼都一樣的話,那對於JVM來說,它就是同一樣東西的了。
有繼承
無 final 修飾
package org.agoncal.sample.jmh; import java.util.Random; /** * Created by emacsist on 2017/6/15. */ public abstract class StringKitAbs { // 生成隨機數字和字母, public String getStringRandom(int length) { String val = ""; Random random = new Random(); // 引數length,表示生成幾位隨機數 for (int i = 0; i < length; i++) { String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 輸出字母還是數字 if ("char".equalsIgnoreCase(charOrNum)) { // 輸出是大寫字母還是小寫字母 // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (random.nextInt(26) + 97); } else if ("num".equalsIgnoreCase(charOrNum)) { val += String.valueOf(random.nextInt(10)); } } return val; } }
有 final 修飾
package org.agoncal.sample.jmh; import java.util.Random; /** * Created by emacsist on 2017/6/15. */ public abstract class StringKitAbsFinal { // 生成隨機數字和字母, public final String getStringRandomFinal(int length) { String val = ""; Random random = new Random(); // 引數length,表示生成幾位隨機數 for (int i = 0; i < length; i++) { String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 輸出字母還是數字 if ("char".equalsIgnoreCase(charOrNum)) { // 輸出是大寫字母還是小寫字母 // int temp = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (random.nextInt(26) + 97); } else if ("num".equalsIgnoreCase(charOrNum)) { val += String.valueOf(random.nextInt(10)); } } return val; } }
測試程式碼
寫一個類來繼承上面的抽象類,以此來測試在繼承中 final 有否對多型中的影響
package org.agoncal.sample.jmh; /** * Created by emacsist on 2017/6/15. */ public class StringKitFinal extends StringKitAbsFinal { }
package org.agoncal.sample.jmh; /** * Created by emacsist on 2017/6/15. */ public class StringKit extends StringKitAbs { }
然後在基準測試中:
@Benchmark public void benchmark() { new StringKit().getStringRandom(32); } @Benchmark public void benchmarkFinal() { new StringKitFinal().getStringRandomFinal(32); }
測試結果
非 final 結果
# JMH version: 1.19 # VM version: JDK 1.8.0_92, VM 25.92-b14 # VM invoker: /srv/jdk1.8.0_92/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.agoncal.sample.jmh.Main.benchmark 中間忽略了預熱及測試過程 Result "org.agoncal.sample.jmh.Main.benchmark": 213462.677 ±(99.9%) 8670.164 ops/s [Average] (min, avg, max) = (135751.428, 213462.677, 264182.887), stdev = 36710.017 CI (99.9%): [204792.513, 222132.841] (assumes normal distribution)
有 final 結果
# JMH version: 1.19 # VM version: JDK 1.8.0_92, VM 25.92-b14 # VM invoker: /srv/jdk1.8.0_92/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: org.agoncal.sample.jmh.Main.benchmarkFinal 中間忽略了預熱及測試過程 Result "org.agoncal.sample.jmh.Main.benchmarkFinal": 213684.585 ±(99.9%) 8571.512 ops/s [Average] (min, avg, max) = (133472.162, 213684.585, 267742.236), stdev = 36292.318 CI (99.9%): [205113.073, 222256.097] (assumes normal distribution)
總對比
# Run complete. Total time: 00:13:35 Benchmark Mode Cnt Score Error Units Main.benchmark thrpt 200 213462.677 ± 8670.164 ops/s Main.benchmarkFinal thrpt 200 213684.585 ± 8571.512 ops/s
它們位元組碼的區別
[12:12:19] emacsist:classes $ diff /tmp/StringKit.log /tmp/StringKitFinal.log 1,5c1,5 < Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKit.class < Last modified 2017-6-16; size 317 bytes < MD5 checksum 7f9b024adc7f39345215e3e8490cafe4 < Compiled from "StringKit.java" < public class org.agoncal.sample.jmh.StringKit extends org.agoncal.sample.jmh.StringKitAbs --- > Classfile /Users/emacsist/Documents/idea/logging/target/classes/org/agoncal/sample/jmh/StringKitFinal.class > Last modified 2017-6-16; size 337 bytes > MD5 checksum f54eadc79a90675d97e95f766ef88a87 > Compiled from "StringKitFinal.java" > public class org.agoncal.sample.jmh.StringKitFinal extends org.agoncal.sample.jmh.StringKitAbsFinal 10,12c10,12 < #1 = Methodref #3.#13 // org/agoncal/sample/jmh/StringKitAbs."<init>":()V < #2 = Class #14 // org/agoncal/sample/jmh/StringKit < #3 = Class #15 // org/agoncal/sample/jmh/StringKitAbs --- > #1 = Methodref #3.#13 // org/agoncal/sample/jmh/StringKitAbsFinal."<init>":()V > #2 = Class #14 // org/agoncal/sample/jmh/StringKitFinal > #3 = Class #15 // org/agoncal/sample/jmh/StringKitAbsFinal 19c19 < #10 = Utf8 Lorg/agoncal/sample/jmh/StringKit; --- > #10 = Utf8 Lorg/agoncal/sample/jmh/StringKitFinal; 21c21 < #12 = Utf8 StringKit.java --- > #12 = Utf8 StringKitFinal.java 23,24c23,24 < #14 = Utf8 org/agoncal/sample/jmh/StringKit < #15 = Utf8 org/agoncal/sample/jmh/StringKitAbs --- > #14 = Utf8 org/agoncal/sample/jmh/StringKitFinal > #15 = Utf8 org/agoncal/sample/jmh/StringKitAbsFinal 26c26 < public org.agoncal.sample.jmh.StringKit(); --- > public org.agoncal.sample.jmh.StringKitFinal(); 32c32 < 1: invokespecial #1 // Method org/agoncal/sample/jmh/StringKitAbs."<init>":()V --- > 1: invokespecial #1 // Method org/agoncal/sample/jmh/StringKitAbsFinal."<init>":()V 38c38 < 0 5 0 this Lorg/agoncal/sample/jmh/StringKit; --- > 0 5 0 this Lorg/agoncal/sample/jmh/StringKitFinal; 40c40 < SourceFile: "StringKit.java" --- > SourceFile: "StringKitFinal.java"
可以看到,除了它們的方法簽名和方法名字不同之外其他的都是一樣的,包括JVM呼叫指令也完全是一樣的。
總結
可以看到它們幾乎是一樣的。
總結
基於上面的基準測試結論,我認為濫用或刻意為了所謂的提升效能,而去為每一個方法儘可能新增 final 的關鍵字是不可取的。使用 final ,更多的應該是根據Java對 final 的語義來定義,而不是隻想著為了提升效能(而且這影響可以忽略不計)而刻意用 final.
使用 final 的情況:
final 變數: 表示只讀(只初始化一次,但可多次讀取)
final 方法:表示子類不可以重寫。(網上認為 final 比非 final 快,就是認為它是在編譯的時候已經靜態繫結了,不需要在執行時再動態繫結。這個可能以前的JVM上是正確的,但在現代的JVM上,這個可以認為沒什麼影響,至少我在基準測試裡是這樣子)
final 類: 它們不能被繼承,而且final類的方法,預設也是 final 的。
關於這個 final 的效能問題,我也Google了下,發現 stackoverflow 上,也有類似的問題:stackoverflow
相關文章
- java中final類 跟final方法Java
- Java final方法Java
- Java中static、final、static final的區別Java
- Java中final與static final的區別Java
- java中的Static、final、Static final各種用法Java
- JAVA finalJava
- Java中final關鍵字Java
- Java空白finalJava
- Java final類Java
- PHP 物件導向 final類與final方法PHP物件
- 探究final在java中的作用Java
- Java中static、final用法小結Java
- Java中final修飾的方法是否可以被重寫Java
- Java final資料Java
- java基礎:finalJava
- Java中final關鍵字如何使用?Java
- java中final修飾符的用法Java
- Java final自變數Java變數
- Java Final關鍵字Java
- [Java物件導向]finalJava物件
- java中的static和final關鍵字Java
- final
- Java 中 final 關鍵字有什麼用Java
- Java中final修飾符都有什麼作用Java
- Java中final、finally、finalize的區別Java
- Java中final,finally,finalize的區別Java
- 深入理解Java中的final關鍵字Java
- Java入門系列之finalJava
- Java 關鍵字之 finalJava
- Java中常見的final類Java
- java之final關鍵字Java
- java中方法的終結者(final關鍵字)Java
- Java中final、finally和finalize的區別Java
- Java中final,finalize和finally的區別Java
- Java併發--final關鍵字Java
- java基礎-關鍵字finalJava
- 請不要說自己是Java程式設計師Java程式設計師
- 請不要說自己是 Java 程式設計師Java程式設計師