1. 程式人生 > >Java中final修飾符(6.4)

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變數就不再是一個變數,而是相當於一個直接量。

  1. 使用final修飾
  2. 在定義該變數時指定了初始值
  3. 該初始值可以在編譯時就被確定下來(賦值表示式只是基本的算術運算,字串連線,沒有訪問變數,呼叫方法時,在編譯時都能確定下來)
    提示: 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
   }
}