Thinking in Java(6-3) final 關鍵字詳解及應用
final 關鍵字詳解及應用
Java 中的 final
關鍵字在不同的情境下具有細微的差異,但核心概念是不可改變。這種不可變性可能出於兩個主要原因:
- 設計目的:為了保持程式的完整性和安全性,防止某些值或方法被修改。
- 效率考量:在某些情況下,可以透過使用
final
來提高程式的執行效率。
本篇文章將深入探討 final
關鍵字在數據、方法、類別以及初始化等方面的詳細應用。
final 數據(Final Data)
許多程式語言都有方式讓編譯器知道某些數據是不可變的。在 Java 中,這可以透過使用 final
關鍵字來實現。
常數與基本型別
- 編譯時常數:必須是基本型別的變數,並在聲明時以
final
關鍵字修飾,同時必須給予初始值。 static final
:當一個變數同時被聲明為static
和final
,它就成為一個不可改變的靜態常數。
範例如下:
public class Constants {
public static final int VALUE_ONE = 9;
private static final int VALUE_TWO = 99;
public static final int VALUE_THREE = 39;
}
VALUE_ONE
、VALUE_TWO
和VALUE_THREE
都是帶有編譯時數值的基本型別,並且是不可改變的常數。VALUE_THREE
被定義為public
,因此可以在類別外部使用。
final 物件引用
當 final
用於物件引用而非基本型別時,final
使該引用恆定不變。也就是說,一旦引用被初始化指向某個物件,就不能再指向其他物件。然而,物件本身的內容是可以被修改的。
重要注意事項:
- 不可改變引用:
final
僅限制引用本身不可改變,但不限制引用的物件內容。 - 無法創建真正的常量物件:Java 並未提供直接的方法來創建完全不可變的物件(除非自行設計不可變的類別)。
這種限制也適用於陣列,因為陣列在 Java 中也是物件。
以下是一個綜合範例:
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; }
// Can be compile-time constants:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
// Typical public constant:
public static final int VALUE_THREE = 39;
// Cannot be compile-time constants:
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++; // Object isn't constant!
fd1.v1 = new Value(9); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(0); // Error: Can't
//! fd1.VAL_3 = new Value(1); // change reference
//! fd1.a = new int[3];
print(fd1);
print("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
print(fd1);
print(fd2);
}
} /* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*/
在這個範例中:
valueOne
和VALUE_TWO
是基本型別的final
變數,並在定義時給予初始值,因此可以作為編譯時常數。i4
和INT_5
也是final
,但它們在執行時期透過隨機數初始化,因此不是編譯時常數。v2
是一個final
物件引用,無法再指向其他物件,但可以修改其內部狀態。
空白 final(Blank Final)
Java 允許宣告空白 final
,即在聲明時未給予初始值的 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
// Blank finals MUST be initialized in the constructor:
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) {
new BlankFinal();
new BlankFinal(47);
}
}
在這個範例中,j
和 p
是空白 final
變數,必須在每個建構子中進行初始化。
final 參數(Final Arguments)
Java 允許在方法的參數列表中將參數指定為 final
。這表示在方法內部無法改變參數引用所指向的物件,但可以改變物件的內容。
範例如下:
class Gizmo {
public void spin() {}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
// void f(final int i) { i++; } // Can't change
// You can only read from a final primitive:
int g(final int i) { return i + 1; }
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
}
在這個範例中,with()
方法的參數 g
被聲明為 final
,因此無法在方法內將 g
指向新的物件。
final 方法
使用 final
修飾方法有兩個主要原因:
- 防止方法被覆寫:將方法鎖定,防止繼承的類別修改其行為。
- 效率考量:在早期的 Java 版本中,宣告為
final
的方法允許編譯器將其轉換為內聯(inline)調用,以提高效率。
然而,現代的 JVM 已經能夠自動優化內聯調用,因此在 Java SE5/6 之後,應該將效率問題交給編譯器和 JVM,只在確實需要防止方法被覆寫時,才將其宣告為 final
。
private 方法隱式為 final
在類別中,所有的 private
方法都被隱式地視為 final
。由於子類別無法訪問 private
方法,因此也無法覆寫它們。
以下是一個範例:
class WithFinals {
// Identical to "private" alone:
private final void f() { print("WithFinals.f()"); }
// Also automatically "final":
private void g() { print("WithFinals.g()"); }
}
class OverridingPrivate extends WithFinals {
private final void f() {
print("OverridingPrivate.f()");
}
private void g() {
print("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate {
public final void f() {
print("OverridingPrivate2.f()");
}
public void g() {
print("OverridingPrivate2.g()");
}
}
public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// You can upcast:
OverridingPrivate op = op2;
// But you can't call the methods:
//! op.f();
//! op.g();
// Same here:
WithFinals wf = op2;
//! wf.f();
//! wf.g();
}
} /* Output:
OverridingPrivate2.f()
OverridingPrivate2.g()
*/
注意:只有當方法是父類別介面的一部分時,覆寫才會發生。當方法是 private
時,它不屬於子類別可見的介面,因此子類別中具有相同名稱的方法並不會覆寫父類別的 private
方法。
final 類別
當將整個類別聲明為 final
時,表示該類別不打算被繼承,也不允許其他類別繼承它。這可能出於設計或安全性的考量,確保類別的行為不被修改。
在 final
類別中:
- 類別內的欄位可以選擇是否宣告為
final
。 - 所有的方法都隱式地被視為
final
,因為無法繼承該類別。
範例如下:
class SmallBrain {}
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
}
在這個範例中,Dinosaur
被宣告為 final
,因此無法被繼承。試圖繼承 Dinosaur
會導致編譯錯誤。
繼承類別的初始化與類別載入
在傳統的程式語言中,初始化的順序必須小心控制。例如,在 C++ 中,如果一個 static
變數在初始化之前被使用,可能會導致問題。
Java 中的類別載入機制
- 類別載入時機:Java 的類別會在首次使用時載入,包括首次訪問
static
成員時。 static
初始化:所有的static
變數和靜態初始化塊都會在類別載入時,按照定義的順序執行一次。
繼承與初始化順序
以下是一個示範繼承與初始化順序的範例:
class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized");
static int printInit(String s) {
print(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle() {
print("k = " + k);
print("j = " + j);
}
private static int x2 =
printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
print("Beetle constructor");
Beetle b = new Beetle();
}
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/
執行步驟解析
- 載入
Beetle
類別:執行Beetle.main()
,載入器開始載入Beetle
的編譯碼。 - 載入父類別
Insect
:發現Beetle
繼承自Insect
,因此先載入Insect
。 - 執行父類別的
static
初始化:執行Insect
中的static
初始化塊和變數。 - 執行子類別的
static
初始化:執行Beetle
中的static
初始化塊和變數。 - 建立物件,執行構造函數:
- 初始化基本型別:將物件中的基本型別設為預設值,物件引用設為
null
。 - 執行父類別的構造函數:呼叫
Insect
的構造函數。 - 執行子類別的實例初始化:執行
Beetle
的實例變數初始化和構造函數。
- 初始化基本型別:將物件中的基本型別設為預設值,物件引用設為
初始化順序的重要性
static
成員的初始化順序:先父類別,後子類別,且按照定義的順序執行。- 實例成員的初始化順序:同樣按照父類別到子類別的順序進行,確保父類別的初始化在子類別之前完成。
這種初始化順序確保了在子類別中使用父類別的成員時,它們已經被正確初始化,避免了潛在的錯誤。
總結
final
關鍵字在 Java 中扮演著重要的角色,提供了控制不可變性的機制。透過 final
,我們可以:
- 宣告不可改變的變數:包括基本型別和物件引用。
- 防止方法被覆寫:提高程式的安全性和穩定性。
- 禁止類別被繼承:確保類別的設計不被修改。
同時,我們也討論了類別的初始化與載入機制,了解了繼承關係中初始化的順序和重要性。
關鍵要點:
final
變數:一旦初始化後,無法再賦予新值(對於物件引用,無法指向新的物件,但可以修改物件內容)。final
方法:防止方法被子類別覆寫。final
類別:禁止類別被繼承。- 初始化順序:
static
成員在類別首次載入時初始化,實例成員在物件建立時按照父類別到子類別的順序初始化。
透過對 final
關鍵字的深入理解,我們可以編寫出更可靠、更安全的 Java 程式碼,充分利用語言提供的特性來構建穩健的應用程式。
Comments ()