1 final基本用法 final:“這是無法改變的" final可以修飾:變數、參數、方法、類 1.1 final修飾變數 修飾變數(變數、局部變數),當變數類型為: 基本類型,一旦被賦值,該值不能被改變。 引用類型,一旦引用被初始化指向一個對象,就不能指向別的對象,但對象內容可以被修改 數據類型 ...
目錄
1 final基本用法
final:“這是無法改變的"
final可以修飾:變數、參數、方法、類
1.1 final修飾變數
修飾變數(變數、局部變數),當變數類型為:
- 基本類型,一旦被賦值,該值不能被改變。
- 引用類型,一旦引用被初始化指向一個對象,就不能指向別的對象,但對象內容可以被修改
- 數據類型:數組也是引用類型
分析以下代碼:
import java.util.Random;
class Value {
int i; // Package access
public Value(int i) { this.i = i; }
}
public class FinalData {
private static Random rand = new Random(47);
private String id;
public FinalData(String id) { this.id = id; }
//編譯時常量
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
public static final int VALUE_THREE = 39;
//非編譯時常量
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
// Arrays:
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // Error: can’t change value
fd1.v2.i++; // OK:引用指向的對象內容可變
fd1.v1 = new Value(9); // OK :非final,引用可變
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn’t constant!
//! fd1.v2 = new Value(0); // Error: final引用不可變
//! fd1.VAL_3 = new Value(1); //Error: final引用不可變
//! fd1.a = new int[3];
System.out.println(fd1);
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
/* 運行結果:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~
說明:
- valuOne和VALUE_TWO:都是編譯期常量,無重大區別。
- VAL_THREE:典型的對常量定義的方式:定義為public,則可以被用於包之外;定義為static,則強調只有一份;定義為final,則說明它是一個常量。註意這種類型常量的命名方式(大寫和下劃線)
- i4和INT_ 5:final變數不代表編譯時可知它的值,可以在運行時初始化值。例如在運行時使用隨機生成的數值來初始化
- v1、v2、VAL_3 說明final引用的特征
- 特別註意:INT_5:不可以通過創建第二個FinalData對象而加以改變。因為它是static的,在裝載時已被初始化,而不是每次創建新對象時都初始化。
1.2 final修飾方法參數
參數:遵循final修飾變數的約束條件,不能在方法中修改它的值或者指向別的對象。
private void finalParam(final Map param){
param = new HashMap();//報錯
param.put("","");//不報錯
}
1.3 final修飾方法
使用final方法的原因:確保在繼承中使方法行為保持不變,並且不會被覆蓋(設計考慮)。
- final修飾的方法不可以重寫(重寫發生在父類與之類)
- final修飾的方法可以重載(同一個類)
以下代碼可以正確運行:
public class FinalExampleParent {
public final void test() {
}
public final void test(String str) {
}
}
final和private:
類中所有的private方法都隱式地指定為final的。由於其它類無法取用private方法,因此無法覆蓋它。可以對private方法添加final修飾,但沒意義。
1.4 final修飾類
當類定義為final時,表示該類不可繼承。
final類的所有方法都是隱式為final,因為無法覆蓋它們
1.5 空白final
定義:被聲明為final但又未給定初值的域。
用途:提供了更大的靈活性:一個類中的final域就可以做到根據對象而有所不同,卻又保持其恆定不變的特性。
class Poppet {
private int i;
Poppet(int ii) { i = ii; }
}
public class BlankFinal {
private final int i = 0; // Initialized final
private final int j; // Blank final
private final Poppet p; // Blank final reference
//空final構造器中初始化
public BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet(1); // Initialize blank final reference
}
public BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet(x); // Initialize blank final reference
}
public static void main(String[] args) {
//空final域在不同情形下賦予不一樣的初值
new BlankFinal();
new BlankFinal(47);
}
}
說明:
- 必須在域的定義處或者每個構造器中對final賦值,這正是fnal域在使用前總是被初始化的原因所在。
- 一個類中的final域可以根據對象而有所不同,卻又保持其不變的特性。
1.6 static final
- 同時是static final 的欄位占據一段不能改變的存儲空間,它必須在定義的時候進行賦值,否則編譯器將不予通過【即使在構造函數中初始化也不行】。
- static修飾的欄位並不屬於一個對象,而是屬於這個類的。【對一個類創建多個對象,其static final 修飾的變數其實是指向同一個值】
2 jvm角度理解final不可變性
一、Javac編譯器
final變數的不變性由Javac編譯時來保證:(只能在編譯期而不能在運行期中檢查)
javac編譯時,進入數據及控制流分析階段時,Flow.flow()會涉及以下檢查:檢查final變數是否有多次賦值,空白final變數是否在構造函數中進行過初始化。
這裡參考:javac final變數未賦值檢測講解
二、JVM類載入
final類的不可變性由jvm進行類載入的校驗階段來保證:
JVM類載入的校驗階段中,對元數據驗證時,包含final語義校驗:
1. 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)
2. 類中的欄位、方法是否與父類產生矛盾(例如覆蓋了父類的final欄位,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不同等)
3 final多線程下可見性
定義:被final修飾的欄位在構造器中一旦被初始化完成,並且構造器沒有把“this”的引用傳遞出去,那麼在其他線程中就能看見final欄位的值。
如代碼所示,變數i與j都具備可見性,它們無須同步就能被其他線程正確訪問。
public static final int i;
public final int j;
static {
i = 0;
// 省略後續動作
}
{
// 選擇在構造函數中初始化
j = 0;
// 省略後續動作
}
解讀:
final欄位如果聲明時賦值,因為只能賦值一次,因此即便存在併發,也能確保只有唯一值
如果在構造函數中賦值,在無引用溢出下,構造函數是線程安全的,因此final欄位也是線程安全
4 final域重排序規則
這方面內容待研究,或者參考:final域重排序規則
5 面試常見問題
5.1 所有的final修飾的欄位都是編譯期常量嗎?
不是
編譯期常量指的就是程式在編譯時就能確定這個常量的具體值
非編譯期常量就是程式在運行時才能確定常量的值 (運行時常量)
public class Test {
//編譯期常量
final int i = 1;
final static int J = 1;
//非編譯期常量
Random r = new Random();
final int k = r.nextInt();
}
k的值由隨機數對象決定,所以不是所有的final修飾的欄位都是編譯期常量,只是k的值在被初始化後無法被更改。
5.2 final類型的類如何拓展?
設計模式中最重要的兩種關係,一種是繼承/實現,另外一種是組合關係。所以當遇到不能用繼承的,應該考慮用組合:
class MyString{
private String innerString;
// ...init & other methods
// 支持老的方法
public int length(){
return innerString.length(); // 通過innerString調用老的方法
}
// 添加新方法
public String toMyString(){
//...
}
}
5.3 如何理解private所修飾的方法是隱式的final?
類中所有的private方法都隱式地指定為final,因為其它類無法調用private方法,因此無法覆蓋它。可以對private方法添加final修飾,但沒意義
參考書籍:《Thinking in Java》 《深入理解java虛擬機》