在能夠通過編譯的前提下,無論局部變量聲明時帶不帶final關鍵字修飾,對其訪問的效率都一樣。
並且:重復訪問一個局部變量比重復訪問一個成員或靜態變量快;即便將其final修飾符去掉,效果也一樣。
例如說,以下代碼:static int foo() { int a = someValueA(); int b = someValueB(); return a + b; // 這里訪問局部變量
}
與帶final的版本:
static int foo() { final int a = someValueA(); final int b = someValueB(); return a + b; // 這里訪問局部變量
}
效果一模一樣,由javac編譯得到的字節碼會是這樣:
invokestatic someValueA:()I istore_0 // 設置a的值
invokestatic someValueB:()I istore_1 // 設置b的值
iload_0 // 讀取a的值
iload_1 // 讀取b的值
iadd ireturn
字節碼里沒有任何東西能體現出局部變量的final與否,Class文件里除字節碼(Code屬性)外的輔助數據結構也沒有記錄任何體現final的信息。既然帶不帶final的局部變量在編譯到Class文件后都一樣了,其訪問效率必然一樣高,JVM不可能有辦法知道什么局部變量原本是用final修飾來聲明的。
但有一個例外,那就是聲明的“局部變量”並不是一個變量,而是編譯時常量的情況:static int foo2() { final int a = 2; // 聲明常量a
final int b = 3; // 聲明常量b
return a + b; // 常量表達式
}
Chapter 4. Types, Values, and Variables
其訪問會按照Java語言對常量表達式的規定而做常量折疊。
Chapter 15. Expressions
實際效果跟這樣的代碼一樣:
static int foo3() { return 5; }
由javac編譯得到對應的字節碼會是:
iconst_5 // 常量折疊了,沒有“訪問局部變量”
ireturn
而這種情況如果去掉final修飾,那么a和b就會被看作普通的局部變量而不是常量表達式,在字節碼層面上的效果會不一樣
static int foo4() { int a = 2; int b = 3; return a + b; }
就會編譯為:
iconst_2 istore_0 // 設置a的值
iconst_3 istore_1 // 設置b的值
iload_0 // 讀取a的值
iload_1 // 讀取b的值
iadd ireturn
但其實這種層面上的差異只對比較簡易的JVM影響較大,因為這樣的VM對解釋器的依賴較大,原本Class文件里的字節碼是怎樣的它就怎么執行;對高性能的JVM(例如HotSpot、J9等)則沒啥影響。這種程度的差異在經過好的JIT編譯器處理后又會被消除掉,上例中無論是 foo3() 還是 foo4() 經過JIT編譯都一樣能被折疊為常量5。
Android里的Dalvik VM雖然是個比較簡單的VM,但Android開發套件里的dexopt也可以用來處理這種final的局部“常量”與“變量”的差異,所以實際性能也不會受多少影響。
還有,先把成員或靜態變量讀到局部變量里保持一定程度的一致性,例如:在同一個方法里連續兩次訪問靜態變量A.x可能會得到不一樣的值,因為可能會有並發讀寫;但如果先有final int x = A.x然后連續兩次訪問局部變量x的話,那讀到的值肯定會是一樣的。這種做法的好處通常在有數據競態但略微不同步沒什么問題的場景下,例如說有損計數器之類的。
最后,其實很多人用這種寫法的時候根本就沒想那么多吧。多半就是為了把代碼寫短一點,為了把一串很長的名字弄成一個短一點的而把成員或靜態變量讀到局部變量里,順便為了避免自己手滑在后面改寫了局部變量里最初讀到的值而加上final來讓編譯器(javac之類)檢查。