Java中final修飾符(6.4)
final關鍵字可用於修飾類,變數和方法。當final修飾變數時,表示該變數一旦獲得初始值就不能重新被賦值。
1. final成員變數
對於final修飾的成員變數而言,一旦有了初始值,就不能被重賦值,如果既沒有在定義成員變數時指定初始值,也沒有在初始化塊,構造器中為成員變數指定初始值,那麼這些成員變數的值一直是系統預設分配的0,“\u0000”,false,null,這些變數就完全失去了意義,因此Java語法規定:final修飾的成員變數必須由程式設計師顯式地指定初始值
歸納起來,final修飾的類變數,例項變數能指定初始值的地方如下:
類變數:必須在靜態初始化塊中指定初始值或宣告該類變數時指定初始值,而且只能在兩個地方的其中之一指定。
例項變數:必須在非靜態初始化塊,宣告該例項變數或構造器中指定初始值,而且只能在三個地方之一指定。
注意:對於final修飾的類變數不能在普通初始化塊中指定初始值,因為類變數在類初始化階段就已經被初始化了,普通初始化塊不能對其重新賦值。
public void changeFinal()
{
//d為final修飾的成員變數,且已經賦過初值
// 普通方法不能Ϊfinal修飾的成員變數賦值
// d = 1.2;
//ch為final修飾的成員變數,未賦過初值
// 普通方法不能Ϊfinal修飾的成員變數指定初始值
// ch = 'a';
}
public class FinalErrorTest { // 定義一個final修飾的例項變數 //系統不會為final成員變數進行預設初始化 final int age; { // age沒有進行初始化,下面程式碼將錯誤 //System.out.println(age); printAge();//此處程式碼時允許的,將輸出0 age = 6; System.out.println(age);//此處將輸出6 } public void printAge(){ System.out.println(age); } public static void main(String[] args) { new FinalErrorTest(); } }
從上面的程式碼可知,final修飾的成員變數在未進行初始化前是不能被直接訪問的。但可以通過方法來訪問,基本上可以斷定這是Java設計上的一個缺陷。
2. final修飾基本資料型別與引用型別變數的區別
final修飾的基本資料型別變數可以在宣告時賦值,或者在後面程式碼中賦值,但只能對其賦值一次,因此final修飾的基本變數不會改變,但對於引用型別變數,它儲存的僅僅是一個地址,final只保證這個引用型別指向的地址不變,即一直引用同一個物件,但引用的物件完全可以改變。
class Person { private int age; public Person(){} // 有引數的構造器 public Person(int age) { this.age = age; } // 省略age的setter,getter方法 public class FinalReferenceTest { public static void main(String[] args) { // final修飾陣列變數,iArr是引用變數 final int[] iArr = {5, 6, 12, 9}; System.out.println(Arrays.toString(iArr)); // 對陣列元素進行排序,合法 Arrays.sort(iArr); System.out.println(Arrays.toString(iArr)); // 對陣列元素進行賦值,合法 iArr[2] = -8; System.out.println(Arrays.toString(iArr)); // 下面語句對iArr重新賦值,非法 // iArr = null; // final修飾Person變數,p是引用變數 final Person p = new Person(45); //改變Person物件的age例項變數,合法 p.setAge(23); System.out.println(p.getAge()); // 下面語句對p重新賦值,非法 // p = null; } }
3. 可執行“巨集替換”的final變數
對一個final變數來說,不管它是類變數,例項變數,還是區域性變數,只要變數滿足下面三個條件這個final變數就不再是一個變數,而是相當於一個直接量。
- 使用final修飾
- 在定義該變數時指定了初始值
- 該初始值可以在編譯時就被確定下來(賦值表示式只是基本的算術運算,字串連線,沒有訪問變數,呼叫方法時,在編譯時都能確定下來)
提示: 1 final修飾符的一個重要用途就是定義“巨集變數”,程式執行時,用到巨集變數的地方直接被換成該變數的值。
注意: 對於例項變數而言,既可以在定義該變數時指定初始值,也可以在非靜態初始化塊,構造器中賦初始值,在這三個地方指定初始債的效果基本一樣,但對於final修飾的例項變數而言,只有在定義該變數時指定初始值才會有“巨集變數”的效果。類變數大體上也一樣
4. final方法
final修飾的方法不可被重寫,若不希望子類重寫父類的某個方法,則可以使用final修飾該方法。
java提供的Object類就有一個final方法:getclass(),因為不希望任何類重寫該方法,所以使用final將這個方法密封起來。但對於該類提供的toString()和equals()方法,都允許子類重寫,因此沒有使用final修飾它。
注意: 1 final修飾的方法是針對於重寫方法方面,所以考慮重寫方面需要注意的地方,比如;若父類的peivate修飾的方法子類無法訪問,也就無法進行重寫,所以當final修飾一個private的方法時,子類中就算寫了一個函式名,引數列表,返回值一樣的方法也不算重寫。
2 final修飾的方法只是不能被重寫,並不是不能被過載。
5. 不可變類
不可變(immutable)類的意思是建立該類例項後,該例項的例項變數是不可改變的。 Java提供的8個包裝類和java.lang.String類都是不可變類,當建立他們的例項後,其例項的例項變數不可改變
Double d = new Double(6.5);
String str = new String("Hello");
上面程式建立了一個Double物件和一個String物件,併為這兩個物件傳入了6.5和"Hello"作為引數,那麼Double類和String類肯定需要提供例項變數來儲存這兩個引數,但程式無法修改這兩個例項變數的值。
自定義不可變類需要滿足下面的規則:
- 使用private和final修飾該類的成員變數
- 提供帶參構造器,用於根據傳入引數來初始化類裡的成員變數
- 僅為該類成員變數提供getter方法,不要為該類提供setter方法,因為普通方法無法修改final修飾的成員變數。
- 如果有必要,重寫Object類的hashCode和equals方法。equals方法根據關鍵成員變數來作為兩個物件是否的標準,除此之外,還應該保證兩個用equals方法判斷為相等的物件的hashCode值也相等。
前面介紹final關鍵字是提到,當使用final修飾引用變數時,僅表示該引用變數不可重新賦值,但引用型別所指向的物件依然可以改變。這就產生了一個問題:當建立不可變類時,如果它包含的成員得型別是可變的,那麼其物件的成員變數的值依然是可以改變的----這個不可變類就是失敗的。
class Name
{
private String firstName;
private String lastName;
public Name(){}
public Name(String firstName , String lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
// 省略firstName,lastName的setter,getter方法
}
public class Person
{
private final Name name;
public Person(Name name)
{
this.name = name;
}
public Name getName()
{
return name;
}
public static void main(String[] args)
{
Name n = new Name("悟空", "孫");
Person p = new Person(n);
// Person物件的name的firstNameֵ值為"悟空"
System.out.println(p.getName().getFirstName());
// 改變Person物件的name的firstNameֵ值
n.setFirstName("八戒");
// Person物件的name的firstNameֵ值被改為"八戒"
System.out.println(p.getName().getFirstName());
}
}
為了保持Person物件的不可變性,必須保護好Person物件的引用型別成員變數,為此對Person類做出如下修改。
public class Person
{
private final Name name;
public Person(Name name)
{
//設定name例項變數為臨時建立的Name物件,
//該物件的firstname和lastname與傳入的name引數的firstname和lastname相同
this.name = new Name(name.getFirstName(),name.getLastName());
}
public Name getName()
{
//返回一個匿名物件,該物件的firstname和lastname
//與該物件裡的name的firstname和lastname相同
return new Name(name.getFirstName(),name.getLastName());
}
public
Person類改寫了設定name例項變數的方法,也改寫了name的getter方法。當程式向Person構造器裡傳入一個Name物件時,該構造器建立Person物件並不是直接利用已有的Name物件(利用已有的物件有風險,因為這個已有的物件是可變的,如果程式改變了這個Name物件,將會導致Person物件也發生變化),而是重新建立一個Name物件來賦給Person物件的name例項變數。
當Person物件返回name變數時,它並沒有直接把name例項變數返回,直接返回name例項變數的值也可能導致它所引用的Name物件被改變。
==如果需要設計一個不可變類,尤其要注意其引用型別的成員變數,如果引用型別的成員變數的類是可變的,就必須採取必要的措施來保護該成員變數所引用的物件不會被修改,==這樣才是真正的不可變類。
6. 快取例項的不可變類
不可變類的例項狀態不可改變,可以很方便地被多個物件所共享。如果程式經常需要使用相同的不可變類例項,則應該考慮快取這種不可變類的例項。畢竟重複建立相同的物件沒有太大意義,而且加大系統開銷,如果可能,應該將已經建立的不可變類的例項進行快取。
快取是設計中一個非常有用的模式,快取實現的方法有很多種,不同的實現方法可能存在較大的效能差別。
class CacheImmutale
{
private static int MAX_SIZE = 10;
//使用陣列來快取已有的例項
private static CacheImmutale[] cache
= new CacheImmutale[MAX_SIZE];
// 記錄快取例項在陣列中的位置,cache[pos-1]是最新快取例項
private static int pos = 0;
private final String name;
//隱藏構造器
private CacheImmutale(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
//通過下面的方法來建立物件
public static CacheImmutale valueOf(String name)
{
// 遍歷已經快取的例項
for (int i = 0 ; i < MAX_SIZE; i++)
{
// 如果已經有相同的例項,則直接返回該快取的例項
if (cache[i] != null
&& cache[i].getName().equals(name))
{
return cache[i];
}
}
// 如果快取池已滿
if (pos == MAX_SIZE)
{
//把快取的第一個物件覆蓋,即把剛剛生成的物件放在快取池最開始的位置
cache[0] = new CacheImmutale(name);
// 把pos設為1
pos = 1;
}
else
{
// 把新建立的物件快取起來,pos加1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if(this == obj)
{
return true;
}
if (obj != null && obj.getClass() == CacheImmutale.class)
{
CacheImmutale ci = (CacheImmutale)obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[] args)
{
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
// 下面程式碼將輸出true
System.out.println(c1 == c2);
}
}
提示: 是否需要隱藏構造器完全取決於系統要求,盲目亂用快取也可能導致系統性能下降,如某物件只用一次,重複使用概率不大,快取該例項就弊大於利。
例如Java提供的java.lang.Integer類,它就採用了上面相同的處理策略,如果採用new構造器來建立物件,則每次返回全新的Integer物件;如果採用ValueOf()方法來建立Integer物件,則會快取該方法建立的物件(由於Integer構造器不會啟動快取,效能較差,Java9已將將該構造器標記為過時)
public class IntegerCacheTest
{
public static void main(String[] args)
{
// 生成新的Integer物件
Integer in1 = new Integer(6);
// 生成新的Integer物件 ,並快取該物件
Integer in2 = Integer.valueOf(6);
//直接從快取中取出Ineger物件
Integer in3 = Integer.valueOf(6);
System.out.println(in1 == in2); // 輸出false
System.out.println(in2 == in3); //輸出true
// 由於Intege只快取-128~127֮的值
// 因此200對應的Integer物件沒有被快取
Integer in4 = Integer.valueOf(200);
Integer in5 = Integer.valueOf(200);
System.out.println(in4 == in5); //輸出false
}
}