類別
前面說明了類別其實就是一種自訂的資料型態,跟 C 的 struct 一樣。但是 class 在裏面加入了方法、權限、繼承、抽象類別、介面等等功能,class 因此強大且複雜。
在 class 內,方法外,也就是下圖紅色框線的地方,一般又稱為 Field 區,存放者物件變數或類別變數。類別變數前面會加 static,物件變數則又稱為非 static 變數。
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」的值並不會被改變。
類別變數
在 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
在上面的圖示中,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”);