問題
final修飾的字段就一定是不能重新賦值嗎?
基礎知識
常量變量是使用常量表達式初始化的原始類型或 String 類型的最終變量。變量是否為常量變量可能對類初始化、二進制兼容性和明確賦值有影響。 —Java 語言規范
實驗
用例源碼-重新賦值
import java.lang.reflect.Field;public class ConstantValues {final int fieldInit = 42;final int instanceInit;final int constructor;{instanceInit = 42;}public ConstantValues() {constructor = 42;}static void set(ConstantValues p, String field) throws Exception {Field f = ConstantValues.class.getDeclaredField(field);f.setAccessible(true);f.setInt(p, 9000);}public static void main(String... args) throws Exception {ConstantValues p = new ConstantValues();set(p, "fieldInit");set(p, "instanceInit");set(p, "constructor");System.out.println(p.fieldInit + " " + p.instanceInit + " " + p.constructor);}}
執行結果
42 9000 9000
如上述執行結果所示,上面有3個被final關鍵字修飾的字段,其中的fieldInit字段賦有初值,其他兩個沒有賦初值,而在后續的通過Java反射機制對上述的3個被final修飾字段重新賦值后,執行結果驚奇的發現賦有初值的fieldInit字段的值沒有被修改,其他兩個沒有賦初值的字段的值發生了修改,那這是因為什么呢?我們可以通過查看通過javac靜態編譯的字節碼一查究竟,如下述代碼所示:
$ javap -c -v -p ConstantValues.class
...final int fieldInit;descriptor: Iflags: ACC_FINALConstantValue: int 42 <---- oh...final int instanceInit;descriptor: Iflags: ACC_FINALfinal int constructor;descriptor: Iflags: ACC_FINAL...
public static void main(java.lang.String...) throws java.lang.Exception;descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGSCode:...41: bipush 42 // <--- Oh wow, inlined fieldInit field43: invokevirtual #18 // StringBuilder.append46: ldc #19 // String " "48: invokevirtual #20 // StringBuilder.append51: aload_152: getfield #3 // Field instanceInit:I55: invokevirtual #18 // StringBuilder.append58: ldc #19 // String ""60: invokevirtual #20 // StringBuilder.append63: aload_164: getfield #4 // Field constructor:I67: invokevirtual #18 // StringBuilder.append70: invokevirtual #21 // StringBuilder.toString73: invokevirtual #22 // System.out.println
通過查看上述字節碼可以看出,被初始化賦值的fieldInit字段其實在javac靜態編譯時已經通過內聯操作賦值了,而對于在JVM動態編譯時不可能重新重寫字節碼,所以從此我們可以看出已經進行初始化賦值的由final關鍵字修飾的字段是不能修改的,而未進行初始化賦值的由final關鍵字修飾的字段卻是可以進行修改的。理論上進行初始化賦值的由final關鍵字修飾的字段性能表現肯定要比沒有進行初始化賦值的由final關鍵字修飾的字段要好,我們可以通過下面的測試用例進行進一步的驗證。
用例源碼-是否有final修飾的已初始化字段
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class FinalInitBench {// Too lazy to actually build the example class with constructor that initializes// final fields, like we have in production code. No worries, we shall just model// this with naked fields. Right?final int fx = 42; // Compiler complains about initialization? Okay, put 42 right here!int x = 42;@Benchmarkpublic int testFinal() {return fx;}@Benchmarkpublic int test() {return x;}
}
執行結果
Benchmark Mode Cnt Score Error Units
FinalInitBench.test avgt 9 1.920 ± 0.002 ns/op
FinalInitBench.test:CPI avgt 3 0.291 ± 0.039 #/op
FinalInitBench.test:L1-dcache-loads avgt 3 11.136 ± 1.447 #/op
FinalInitBench.test:L1-dcache-stores avgt 3 3.042 ± 0.327 #/op
FinalInitBench.test:cycles avgt 3 7.316 ± 1.272 #/op
FinalInitBench.test:instructions avgt 3 25.178 ± 2.242 #/opFinalInitBench.testFinal avgt 9 1.901 ± 0.001 ns/op
FinalInitBench.testFinal:CPI avgt 3 0.285 ± 0.004 #/op
FinalInitBench.testFinal:L1-dcache-loads avgt 3 9.077 ± 0.085 #/op <--- !
FinalInitBench.testFinal:L1-dcache-stores avgt 3 4.077 ± 0.752 #/op
FinalInitBench.testFinal:cycles avgt 3 7.142 ± 0.071 #/op
FinalInitBench.testFinal:instructions avgt 3 25.102 ± 0.422 #/op
由上述通過perform 執行結果可以看出,都進行了初始化的兩個字段,有final修飾的字段的性能要更好。那這是因為什么呢?我們可以通過匯編代碼進行查證,具體如下:
# test
...
1.02% 1.02% mov 0x10(%r10),%edx ; <--- get field x
2.50% 1.79% nop
1.79% 1.60% callq CONSUME
...# testFinal
...
8.25% 8.21% mov $0x2a,%edx ; <--- just use inlined "42"
1.79% 0.56% nop
1.35% 1.19% callq CONSUME
...
通過上述的匯編代碼可以看出,由final修飾的字段在執行匯編指令過程中并沒有進行字段加載,而只是引入字節碼中的內聯常量,這就是性能提升的關鍵點。
用例源碼-是否有final修飾的未初始化字段
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class FinalInitCnstrBench {final int fx;int x;public FinalInitCnstrBench() {this.fx = 42;this.x = 42;}@Benchmarkpublic int testFinal() {return fx;}@Benchmarkpublic int test() {return x;}
}
執行結果
Benchmark Mode Cnt Score Error Units
FinalInitCnstrBench.test avgt 9 1.922 ± 0.003 ns/op
FinalInitCnstrBench.test:CPI avgt 3 0.289 ± 0.049 #/op
FinalInitCnstrBench.test:L1-dcache-loads avgt 3 11.171 ± 1.429 #/op
FinalInitCnstrBench.test:L1-dcache-stores avgt 3 3.042 ± 0.031 #/op
FinalInitCnstrBench.test:cycles avgt 3 7.301 ± 0.445 #/op
FinalInitCnstrBench.test:instructions avgt 3 25.235 ± 1.732 #/opFinalInitCnstrBench.testFinal avgt 9 1.919 ± 0.002 ns/op
FinalInitCnstrBench.testFinal:CPI avgt 3 0.287 ± 0.014 #/op
FinalInitCnstrBench.testFinal:L1-dcache-loads avgt 3 11.170 ± 1.104 #/op
FinalInitCnstrBench.testFinal:L1-dcache-stores avgt 3 3.039 ± 0.864 #/op
FinalInitCnstrBench.testFinal:cycles avgt 3 7.278 ± 0.394 #/op
FinalInitCnstrBench.testFinal:instructions avgt 3 25.314 ± 0.588 #/op
由上述執行結果可知,對于未進行初始化,不管是否有final關鍵字修飾的字段,這兩種情況執行的性能表現是一樣的。
總結
由final關鍵字修飾的字段需要進行初始化賦值。