類別與建構子

      在〈類別與建構子〉中尚無留言

類別

前面說明了類別其實就是一種自訂的資料型態,跟 C 的 struct 一樣。但是 class 在裏面加入了方法、權限、繼承、抽象類別、介面等等功能,class 因此強大且複雜。

在 class 內,方法外,也就是下圖紅色框線的地方,一般又稱為 Field 區,存放者物件變數或類別變數。類別變數前面會加 static,物件變數則又稱為非 static 變數。

java_class1

New

產生一個物件又稱為實例化 (Instance),假如有一代碼如下

class Pokemon{
    int level;
    double w;
    Pokemon next;
}

當下達 new Pokemon(),其實就是跟系統下達 malloc(16) 取得 16byte 的空間,其中 4byte規 畫給level, 8byte 給 w,最後是 4 byte 給 next 存放物件的 hashcode。

物件變數

所以當 new 3 次時,就會如下圖所示,產生 3 個物件,每個物件都會各自獨立的 level、w、及 next。class好比是一個模具,使用此模具製作出三個完成品。

level、w、next 三個變數會降下到物件中,稱為物件變數。存取物件 1 的 level,就要使用 “物件1.level” ,中間要加 “.”。每個物件的物件變數都是獨立的,當改變「物件1.leve」時,「物件2.leve」的值並不會被改變。

java_class2

類別變數

在 Field 區宣告的變數如果有加上 static,則稱為類別變數,此變數屬於類別所擁有。在如下的程式碼中,p1.level,p2.levle 皆為獨立的物件變數。但 count 則為類別變數不同,p1.count 一更改後,p2.count 也一併跟著更改。

public class Test {
    public static void main(String[] args){
        Pokemon p1=new Pokemon();
        Pokemon p2=new Pokemon();
        p1.count=100;
        p1.level=10;
        p2.count=200;
        p2.level=20;
        System.out.printf("p1.level=%d, p1.count=%d\n", p1.level, p1.count);
        System.out.printf("p2.level=%d, p2.count=%d\n", p2.level, p2.count);
    }
}
class Pokemon{
    static int count;
    int level;
}
結果 : 
p1.level=10, p1.count=200
p2.level=20, p2.count=200

java_static

在上面的圖示中,static int count 並不會下降到物件中,而是存於類別之中的。

存取類別變數

count 是類別變數,要存取類別變數的方法必需是 Pokemon.count。

雖說 p1.count 也可以存取,但這是物件導向設計上的缺失。嚴格說, p1.count 其實的是錯誤的。因為 count 是大家所共同擁有,憑什麼說 count 是 p1的。但這個 bug 已錯了 30 幾年了,若立即更正會造成早期寫出來的程式不相容,所以一直沒修正。所以請盡量不要使用 p1.count,否則會造成日後程式碼維護困難。

請注意在 C# 中使用 p1.count 是被禁止的。

生命周期

太陽已存在了46億年,而人類頂多也才出現不超過十萬年。所以太陽不是為了人類而掛在天空的。

static 變數就像太陽一樣,打從程式載入時期,就存在 Ram 中。而程式的載入期到程式的執行時期,差了數千百萬奈秒。

static 方法不能存取非 static 方法及變數。意思是 static 方法早就存在了,而非 static 方法及變數要到執行時期才可能會出現。所以叫 static 去存取尚未存在的東西,是不可能的事。

建構子

當物件被 new 出時會自動執行的方法,稱為建構子 (Constructor)。習慣會把物件內需要初始化的變數放在建構子中執行。建構子是一個特殊的方法,名稱必需與類別名一模一樣,而且沒有返回值,甚至連 void 都沒有。

    [modifiers] class ClassName{
        [modifiers] ClassName([arguments]){
        }
    }
    class Pokemon{
        private int level;
        private float weight;
        public Pokemon(){
            weight=10;
            level=10;
        }    

為什麼不可以有返回值呢? 仔細思考下面的代碼

Pokemon p1=new Pokemon();

右邊的 Pokemon() 就是去執行建構子這個方法。 “=” 是指定運算子,意思是右邊的值放到左邊的 p1。而右邊的值是什麼? 就是這個物件的雜湊碼 (hashcode)。這個雜湊碼是由系統產生,然後傳給 p1。所以如果建構子又有傳回值,就會跟雜湊碼相衝。因此才會規定建構子不淮有回傳值,連 void也不可以有。

預設建構子

在此之前我們都沒有撰寫建構子,但依然可以產生物件。這是因為類別若無任何建構子時,編譯器會自動加入預設建構子。因此就算不寫建構子一樣可以new 物件。但若有自訂建構子時,則編譯器就不會幫我們填入預設建構子。

    public Shirt(){} //預設建構子
    public Shirt(char colorCode){setColorCode(colorCode);}
    public Shirt(char colorCode, double price){
        this(colorCode);
        setPrice(price);
    }

建構子重載

若有加入參數的建構子,稱為自訂建構子。預設建構子與自訂建構子可以同時存在,就是方法重寫(Overload)的機制。

如下的代碼,表示可以直接將 level 放入建構子,如 new Pokemon(10)。但也可以使用 new Pokemon(),此時表示如果不傳入level, 則執行 this(1),調用自訂建構子,level 使用預設為 1 的值。

class Pokemon{
    private int level;
    public Pokemon(){
        this(1);
    }
    public Pokemon(int level){
        this.level=level;
    }
}

封裝

上述的 class Pokemon{},想像由任天堂公司開發,而 main() 這部份是另一間軟体開發商所開發的。軟体開發商通常都會跟任天堂、Facebook、Line 等公司簽定 NDA (保密協定) 並支付龐大的簽約金才可獲取相關技術及 SDK(Software Development Kit)。所以 class Pokemon 就是 SDK的一部份。

軟体開發商依 SDK 寫了 Pokemon p1=new Pokemon(),此時可以直接使用 p1.level=5000,這隻神奇寶貝的等級一下子就拉高變成了超級塞亞人,這不就違反了等級需要一步一步訓練的規定嗎。

任天堂為了防止軟体開發商使用此招,就會把 level 加上 private,即

class Pokemon{
    private int level;
}

將物件變數使用 private 保護起來,如此軟体開發商就無法使 p1.level=5000。

使用 private 保護的物件變數,對軟体開發商而言尤如垃圾一樣,無法使用。所以通常會再加入 setter/getter存取子。

class Pokemon{
   private int level;
   double w;
   public void setLevel(int l){
       if (l>100)this.level=1;
       else this.level=l;
   }
   public int getLevel(){
       return level;
   }
}

 public 修飾子

標示為 public 的物件變數,意味著可以在 main 裏 new 出物件後,直接由物件去修改其值。若沒加 public,也沒加 private,稱為預設權限。預設權限在相同 package 裏如同 public,在不同 package 如同 private。

private 修飾子

為了解決上述容易被修改成不正常的神奇寶貝,就要將其物件變數改為 private,將之封印起來,不讓玩家可以隨便更改,等到需要存取時,再用 public 方法將之解開封印。

以上可知,重要之類別變數設為 public 會造成危險。方法亦可宣告為 public 或 private,其用意同上。

 Set & Get 方法

使用 setter/getter 存取子已成為物件導向封裝的習慣,因為可以在 setter 方法中檢查設定的值是否正確。

static 方法

又稱類別方法,比如 java.lang.Math 就包含了很多的 static 方法。static 方法可以在子類別中隱藏,但不是覆蓋喔。

呼叫 static 方法時,也是直接使用類別名稱,如Math.random()。

static import

import static java.lang.Math.random;

有了如上宣告,就可以直接使用double d=random();

import static java.lang.System.*;

則可以直接使用out.println();

永久不變的變數

變數宣告為 private 時,可以在類別內使用 public 方法改變其值。但若想建立一個永不能變的變數時,比如帳號號碼, 就必需把 public setter 的方法拿掉。但一拿掉,那如何初使化帳號號碼值呢?  解決方法就是使用建構子,在new 物件時,立即初使化值。

class Account{
    private int accountNumber;
    public Account(int number){
        accountNumber=number;
    }
}

final

final 方法

方法可以被宣告為 final,如

public final void printMessage(){}

將方法宣告為 final 的目的,就是禁止被子類別覆蓋。

final 類別

class 也可以宣告為 final,其目的就是禁止被繼承。

final 變數

final 變數在初始化後,就不能改變其值。final 變數可以加在類別欄位,方法的參數,區域變數三個地方。

加在類別欄位中,即為常數的意思,請特別注意 : 類別欄位加上 final 時,如果在宣告時沒有初始化,就一定要在建構子中初始化,否則會編譯錯誤。

static 變數也可以加 final,但一宣告就一定要初始化,通常會用大寫或底線。

何時要避免Constants

底下的代碼中,POWER_SUSPEND 限定這個變數為 2,且不能改變其值

class Computer{
    public static final POWER_SUSPEND=2;
    public void setState(int x){
        ..............
    }
}

class Main{
    public static void main(String[] args){
        Computer comp=new Computer();
        comp.setState(Computer.POWER_SUSPEND);
        comp.setState(100);
    }
}

上面代碼中,comp.setState(Computer.POWER_SUSPEND) 可以編譯。

而 comp.setState(42) 一樣可以編譯,所以需在 setState() 裏還是要檢查傳入的值是否在範圍之內。

列舉(Enumerations)

列舉用來定義新型的資料型別,實際上就是一種類別,可單獨撰寫,也可以當成內部類別。

enum PowerState{
    OFF,
    ON,
    SUSUPEND;
}

使用方式

comp.setState(PowerState.OFF); <==直接用OFF,不是轉成數字。

public void setState(PowerState state){
    switch(state){
      case OFF:...
     }
}

複雜的列舉

列舉可以有欄位,方法及 private 建構子

enum PowerState{
     OFF("The power is off"), //呼叫建構子, 初始化public static OFF 參考
     ON("The power is on"),
     SUSUPEND("The power usage is low");
     private String description;
     private PowerState(String d){
          description=d;
     }
     public String getDescription(){
          return description;
     }
}
public class Test{
     public static void main(String[] args) {
          System.out.println(PowerState.OFF);
          PowerState s=PowerState.ON;
          System.out.println(s.getDescription());
     }
}

列舉的建構子只能是 private 或預設,因為列舉不能 new 出新物件。

enum 只有當成內部類別才可以加上 static,不過編譯器會自動幫內部類別加上 static final。若是當成外部類別,而且有加上 public 的話,則檔名一樣要跟 enum 類別名相同。

假設有一個 enum 如下

enum Week{
    Sunday, 
    Monday, 
    Tuesday, 
    Wednesday, 
    Thrusday, 
    Friday, 
    Saturday;
}

若為外部類別,則會編譯出Week.class,若是內部類別,則會編譯成Test$Week.class。列舉的 class 都是 final 的,裏面的值全都是 public static final,如下

public static final Week Sunday;
public static final Week Monday;

取得單一值

Week week=Week.Sunday

取得全部的值

底下的代碼可以取得 Week 裏所有的值,產生 week陣列。

Week[] week=Week.values();

使用valueOf(String)

Week week=Week.valueOf(“Sunday”);

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *