使用 class 定義類別

在物件導向設計中,物件並不會憑空產生,您必須識別出問題中的物件,並對這些物件加以定義,您要定義一個規格書,在 Java 中這個規格書稱之為「類別」(Class),您使用類別定義出物件的規格書,之後根據類別來建構出一個個的物件,然後透過物件所提供的操作介面來與程式互動。

在 Java 中使用 "class" 關鍵字來定義類別,使用類別來定義一個物件(Object)時,會考慮這個物件可能擁有的「屬性」(Property)與「方法」(Method)。屬性是物件的靜態表現,而方法則是物件與外界互動的動態操作。

舉個例子來說,若您的問題中會有「帳戶」這個物件,在分析了您的問題之後,您為「帳戶」這個物件定義了 Account 類別。 範例 7.1 Account.java

public class Account { private String accountNumber; private double balance;

public Account() {
    this("empty", 0.0);
}

public Account(String accountNumber, double balance) {
    this.accountNumber = accountNumber;
    this.balance = balance;
}

public String getAccountNumber() {
    return accountNumber;
}

public double getBalance() {
    return balance;
}

public void deposit(double money) {
    balance += money;
}

public double withdraw(double money) {
    balance -= money;
    return money;
}

}

定義類別

首先看到範例中的 "class",這是 Java 中用來定義類別的關鍵字,記得一個類別的定義是這麼作的:

public class Account { // 實作內容 }

Account 是您為類別取的名稱,由於這個類別使用 "public" 關鍵字加以修飾,所以檔案的主檔名必須與類別名稱相同,也就是檔案要取名為 "Account.java",這是規定,在一個檔案中可以定義數個類別,但只能有一個類別被設定為 "public",檔案名稱主檔名必須與這個 public 的類別同名,例如 Account.java 中可以有以下的內容:

public class Account { // 檔案必須是Account.java // 實作內容 } class SomeClass { //實作內容 } class OtherClass { //實作內容 }

定義成員

在類別中的資料及互動方法,統稱其為「類別成員」(Class member),範例 7.1 中的 accountNumber、balance 成員是「資料成員」(Field member),getAccountNumber() 與 getBalance() 是「方法成員」(Method member),在定義資料成員時可以指定初值,如果沒有指定初值,則會有預設值,資料成員如果是基本型態,則預設值與表 5.1 所列出的相同,如果是物件型態,則預設值為 null,也就是不參考任何的物件。

注意到 "public" 這個關鍵字,這表示所定義的成員可以使用宣告的物件名稱加上 '.' 運算子來直接呼叫,也稱之為「公用成員」或「公開成員」。"private" 這個關鍵字用來定義一個「私用成員」,私用成員不可以透過參考名稱加上"."直接呼叫,又稱之為「私有成員」。

一個類別中的資料成員,若宣告為 "private",則其可視範圍(Scope)為整個類別內部,由於外界無法直接存取私用成員,所以您要使用兩個公開方法 getAccountNumber() 與 getBalance() 分別傳回其這兩個成員的值。

定義建構方法

與類別名稱同名的方法稱之為「建構方法」(Constructor),也有人稱之為「建構子」,它沒有傳回值,建構方法的作用是讓您建構物件的同時,可以同時初始一些必要的資訊,建構方法可以被「重載」(Overload),以滿足物件生成時各種不同的初始需求

定義好 Account 類別之後,您就可根據這個類別來建構物件,也就是產生 Account 類別的實例,建構物件時要使用 "new" 關鍵字,顧名思義,就是根據所指定的類別(規格書)「新建」一個物件:

Account account1 = new Account(); Account account2 = new Account("123-4567", 100.0);

7.1.3 類別成員(Class member)

在 Java 中,一個類別可以定義資料成員(Field)及方法(Method) 成員,在 Java 中,類別成員可用的存取權限修飾詞有 "public"、"protected"、"private" 三個,如果在宣告成員時不使用存取修飾詞,則預設以「套件」(package)為存取範圍,也就是說在 package 外就無法存取

再來看到方法(Method)成員,範例 7.1 的每一個方法被宣告為 "public",表示這些方法可以藉由物件的參考名稱加上 "." 直接呼叫,一個方法成員為一小個程式片段或一個執行單元(Unit),這個程式片段可重複被呼叫使用,並可傳入引數或傳回一個表示執行結果的數值,一個方法成員的基本宣告與定義方式如下 :

存取修飾 傳回值型態 方法名稱(參數列) {   // 實作   return 傳回值; } 參數列用來傳入方法成員執行時所需的資料,如果傳入的引數是基本資料型態(Primitive data type),則會將值複製至參數列上的參數,如果傳入的引數是一個物件,則會將參數列上宣告的參數參考至指定的物件。

方法區塊中可以宣告變數(Variable),參數在方法區塊執行結束後就會自動清除,如果方法中宣告的變數名稱與類別資料成員的名稱同名,則方法中的變數名稱會暫時覆蓋資料成員的作用範圍;參數列上的參數名稱也會覆蓋資料成員的作用範圍,

如果此時要在方法區塊中使用資料成員,可以使用 "this" 關鍵字來特別指定

關於THIS

範例 7.3 MethodMember.java

public class MethodMember { public static void main(String[] args) { MethodDemo methodDemo = new MethodDemo();

    methodDemo.scopeDemo(); // 對data 資料成員不會有影響
    System.out.println(methodDemo.getData());

    methodDemo.setData(100); // 對data 資料成員不會有影響
    System.out.println(methodDemo.getData());
}

}

class MethodDemo { private int data = 10;

public void scopeDemo() { // void 表示沒有傳回值
    int data = 100;
}

public int getData() {
    return data;
}

public void setData(int data) { // void 表示沒有傳回值
    data = data; // 這樣寫是沒用的
    // 寫下面這個才有用
    // this.data = data;
}

}

其實您使用參考名稱來呼叫物件的方法成員時,程式會將物件的參考告知方法成員,而在方法中所撰寫的每一個資料成員其實會隱含一個 this 參考名稱,這個 this 名稱參考至呼叫方法的物件,當您呼叫 getBalance() 方法時,其實您相當於執行:

public double getBalance() { return this.balance; }

所以當使用account1並呼叫getBalance()方法時,this所參考的就是account1所參考的物件,而account2並呼叫getBalance()方法時,this所參考的就是account2所參考的物件,所以getBalance()可以正確的得知該傳回哪一個物件的balance 資料。

7.1.4 建構方法(Constructor)

在定義類別時,您可以使用「建構方法」(Constructor)來進行物件的初始化,在 Java 中建構方法是與類別名稱相同的公開方法成員,且沒有傳回值,例如:

public class SafeArray { // .. public SafeArray() { // 建構方法 // .... } public SafeArray(參數列) { // // .... } }

在範例 7.4 示範了實作簡單的「安全的陣列」,您所定義的陣列類別可以動態配置陣列長度,並可事先檢查存取陣列的索引是否超出陣列長度,在這個陣列類別中,您還實作了幾個簡單的功能,像是傳回陣列長度、設定陣列元素值、取得陣列元素值等。 範例 7.4 SafeArray.java

public class SafeArray { private int[] arr;

public SafeArray() {
    this(10); // 預設 10 個元素
}

public SafeArray(int length) { 
    arr = new int[length]; 
}

public void showElement() {
    for(int i : arr) {
        System.out.print(i + " ");
    }
}

public int getElement(int i) { 
    if(i >= arr.length || i < 0) { 
        System.err.println("索引錯誤"); 
        return 0; 
    } 

    return arr[i]; 
}

public int getLength() { 
    return arr.length; 
}

public void setElement(int i, int data) { 
    if(i >= arr.length || i < 0) { 
        System.err.println("索引錯誤"); 
        return; 
    }

    arr[i] = data; 
} 

}

如果您不指定引數的話,就會使用無參數的建構方法來配置 10 個元素的陣列,您也可以由指定的長度來配置陣列;您在無參數的建構方法中使用 this(10),這會呼叫另一個有參數的建構方法,以避免撰寫一些重複的原始碼。

7.1.6 關於 static

對於每一個基於相同類別所產生的物件而言,它們會擁有各自的資料成員,然而在某些時候,您會想要這些物件擁有共享的資料成員,舉個例子來說,如果您設計了一個 Ball 類別,當中打算使用到圓周率PI這個資料,因為對於任一個 Ball 的實例而言,圓周率都是相同的,您不需要讓不同的 Ball 實例擁有各自的圓周率資料成員。

您可以將 PI 資料成員宣告為 "static",被宣告為 "static" 的資料成員,又稱「靜態資料成員」,靜態成員是屬於類別所擁有,而不是個別的物件,您可以將靜態成員視為每個物件實例所共享的資料成員。要宣告靜態資料成員,只要在宣告資料成員時加上 "static" 關鍵字就可以了,例如:

public class Ball { public static double PI = 3.14159; // 宣告static資料 ... }

靜態成員屬於類別所擁有,可以在不使用名稱參考下,直接使用類別名稱加上'.'運算子來存取靜態資料成員,不過靜態資料成員同樣遵守 "public"、"protected" 與 "private" 的存取限制,所以若您要直接存取靜態資料成員,必須注意它的權限,例如必須設定為 "public" 成員的話就可以如下存取:

System.out.println("PI = " + Ball.PI);

通常建議使用類別名稱加上 '.' 運算子來存取,一方面也可以避免與非靜態資料成員混淆,例如下面的方式是不被鼓勵的:

Ball ball = new Ball(); System.out.println("PI = " + ball.PI);

與靜態資料成員類似的,您也可以宣告方法成員為 "static" 方法,又稱「靜態方法」,被宣告為靜態的方法通常是作為工具方法,例如在 Ball 類別上增加一個角度轉徑度的方法 toRadian():

public class Ball { ... public static double toRadian(double angle) { return 3.14159 / 180 * angle; } }

與靜態資料成員一樣的,您可以透過類別名稱使用'.'運算子來存取 "static" 方法,當然要注意權限設定,例如設定為 "public" 的話可以如下存取:

System.out.println("角度90等於徑度" + Ball.toRadian (90));

靜態資料與靜態方法的作用通常是為了提供共享的資料或工具方法,例如將數學常用常數或計算公式,以 "static" 宣告並撰寫,之後您可以把這個類別當作工具類別,透過類別名稱來管理與取用這些靜態資料或方法,例如像 Java SE 所提供的 Math 類別上,就有 Math.PI 這個靜態常數,以及 Math.Exp()、Math.Log()、Math.Sin() 等靜態方法可以直接使用,另外還有像 Integer.parseInt()、Integer. MAX_VALUE 等也都是靜態方法與靜態資料成員的實際例子。

由於靜態成員是屬於類別而不是物件,所以當您呼叫靜態方法時,並不會傳入物件的參考,所以靜態方法中不會有 this 參考名稱,由於沒有 this 名稱,所以在 Java 的靜態方法中不允許使用非靜態成員,因為沒有 this 來參考至物件,也就無法辨別要存取的是哪一個物件的成員,事實上,

如果您在靜態方法中使用非靜態資料成員,在編譯時就會出現以下的錯誤訊息:

non-static variable test cannot be referenced from a static context

或者是在靜態方法中呼叫非靜態方法,在編譯時就會出現以下的錯誤訊息:

non-static method showHello() cannot be referenced from a static context

關於方法

在對定義類別有了瞭解之後,接下來再深入討論類別中的方法成員,在 Java 中,您可以「重載」(Overload)同名方法,而在 J2SE 5.0 之後,您還可以提供方法不定長度引數(Variable-length Argument),當然,最基本的您要知道遞迴(Recursive)方法的使用,最後還要討論一下 finalize() 方法,並從中瞭解一些 Java「垃圾收集」(Garbage collection)的機制。

7.2.1 重載(Overload)方法

Java 支援方法「重載」(Overload),又有人譯作「超載」、「過載」,這種機制為類似功能的方法提供了統一的名稱,但可根據參數列的不同而自動呼叫對應的方法。

方法重載的功能使得程式設計人員能較少苦惱於方法名稱的設計,以統一的名稱來呼叫相同功能的方法,方法重載不僅可根據傳遞引數的資料型態不同來呼叫對應的方法,參數列的參數個數也可以用來設計方法重載,例如您可以這麼重載 someMethod() 方法:

public class SomeClass { // 以下重載了someMethod()方法 public void someMethod() { // ... } public void someMethod(int i) { // ... } public void someMethod(float f) { // ... } public void someMethod(int i, float f) { // ... } }

要注意的是返回值型態不可用作為方法重載的區別根據,例如以下的方法重載是不正確的,編譯器仍會將兩個 someMethod() 視為重複的定義:

public class SomeClass { public int someMethod(int i) { // ... return 0; } public double someMethod(int i) { // ... return 0.0; } }

編譯器在處理重載方法、裝箱問題及「不定長度引數」時,會依下面的順序來尋找符合的方法:

找尋在還沒有裝箱動作前可以符合引數個數與型態的方法
嘗試裝箱動作後可以符合引數個數與型態的方法
嘗試設有「不定長度引數」並可以符合的方法
編譯器找不到合適的方法,回報編譯錯誤

7.2.2 不定長度引數

public class MathTool { public static int sum(int... nums) { // 使用...宣告參數 int sum = 0; for(int num : nums) { sum += num; } return sum; } }

要使用不定長度引數,在宣告參數列時要於型態關鍵字後加上 "...",而在 sum() 方法的區塊中您可以看到,實際上 nums 是一個陣列,編譯器會將參數列的 (int... nums) 解釋為 (int[] nums),您可以如範例 7.11 的方式指定各種長度的引數給方法來使用。

編譯器會將傳遞給方法的引數解釋為 int 陣列傳入至 sum() 中,所以實際上不定長度引數的功能也是J2SE 5.0所提供的「編譯蜜糖」(Compiler Sugar)。

在方法上使用不定長度引數時,

記得必須宣告的參數必須設定在參數列的最後一個

,例如下面的方式是合法的:

public void someMethod(int arg1, int arg2, int... varargs) { // .... }

7.2.3 遞迴方法

「遞迴」(Recursion)是在方法中呼叫自身同名方法,而呼叫者本身會先被置入記憶體「堆疊」(Stack)中,等到被呼叫者執行完畢之後,再從堆疊中取出之前被置入的方法繼續執行。堆疊是一種「先進後出」(First in, last out)的資料結構,就好比您將書本置入箱中,最先放入的書會最後才取出。 Java 支援遞迴,遞迴的實際應用很多,舉個例子來說,求最大公因數就可以使用遞迴來求解,範例 7.12 是使用遞迴來求解最大公因數的一個實例。

import java.util.Scanner;

public class UseRecursion { public static void main(String[] args) { Scanner scanner = new Scanner(System.in);

    System.out.println("輸入兩數:"); 
    System.out.print("m = "); 
    int m = scanner.nextInt();

    System.out.print("n = "); 
    int n = scanner.nextInt();

    System.out.println("GCD: " + gcd(m, n)); 
} 

private static int gcd(int m, int n) { 
    if(n == 0) 
        return m; 
    else 
        return gcd(n, m % n); 
} 

}

執行結果:

輸入兩數: m = 10 n = 20 GCD: 10

使用遞迴好還是使用迴圈求解好?這並沒有一定的答案。由於遞迴本身有重複執行與記憶體堆疊的特性,所以若在求解時需要使用到堆疊特性的資料結構時,使用遞迴在設計時的邏輯會比較容易理解,程式碼設計出來也會比較簡潔,然而遞迴會有方法呼叫的負擔,因而有時會比使用迴圈求解時來得沒有效率,但迴圈求解時若使用到堆疊時,通常在程式碼上會比較複雜。

7.2.4 垃圾收集

在解釋「垃圾收集」(Garbage collection)之前,要先稍微提一下 C++ 中對物件資源的管理,以利待會瞭解 Java 的物件資源管理機制。

在 C++ 中,使用 "new" 配置的物件,必須使用 "delete" 來清除物件,以釋放物件所佔據的記憶體空間,如果沒有進行這個動作,若物件不斷的產生,記憶體就會不斷的被物件耗用,最後使得記憶體空間用盡,在 C++ 中有所謂的「解構方法」(Destructor),它會在物件被清除前執行,然而使用 "delete" 並不是那麼的簡單,如果不小心清除了尚在使用中的物件,則程式就會發生錯誤甚至整個崩潰(Crash),如何小心的使用 "new" 與 " delete",一直是 C++ 中一個重要的課題。

在 Java 中垃圾收集的時機何時開始您並無法得知,可能會在記憶體資源不足的時候,或是在程式執行的空閒時候,您可以建議執行環境進行垃圾收集,但也僅止於建議,如果程式當時有優先權更高的執行緒(Thread)正在進行,則垃圾收集並不一定會馬上進行。

在 Java 中並沒有解構方法,在 Java 中有 finalize() 這個方法,它被宣告為 "protected",finalize() 會在物件被回收時執行,但您不可以將它當作解構方法來使用,因為不知道物件資源何時被回收,所以也就不知道 finalize() 真正被執行的時間,所以無法立即執行您所指定的資源回收動作,但您可以使用 finalize() 來進行一些相關資源的清除動作,如果這些動作與立即性的收尾動作沒有關係的話。

如果您確定不再使用某個物件,您可以在參考至該物件的名稱上指定 "null",表示這個名稱不再參考至任何物件,不被任何名稱參考的物件將會被回收資源,您可以使用 System.gc() 建議程式進行垃圾收集,如果建議被採納,則物件資源會被回收,回收前會執行 finalize() 方法。

範例7.13簡單的示範了finalize()方法的使用。

 範例7.13 GcTest.java

public class GcTest { private String name;

public GcTest(String name) { 
    this.name = name; 
    System.out.println(name + "建立"); 
} 

// 物件回收前執行 
protected void finalize() { 
    System.out.println(name + "被回收"); 
} 

}

使用範例 7.14來作個簡單的執行測試。 範例 7.14 UseGC.java

public class UseGC { public static void main(String[] args) { System.out.println("請按Ctrl + C終止程式........");

    GcTest obj1 = new GcTest("object1"); 
    GcTest obj2 = new GcTest("object2"); 
    GcTest obj3 = new GcTest("object3"); 

    // 令名稱不參考至物件 
    obj1 = null; 
    obj2 = null; 
    obj3 = null; 

    // 建議回收物件 
    System.gc(); 

    while(true); // 不斷執行程式
} 

}

在程式中您故意加上無窮迴圈,以讓垃圾收集在程式結束前有機會執行,藉以瞭解垃圾收集確實會運作,程式執行結果如下所示:

請按Ctrl + C終止程式........ bject1建立 bject2建立 bject3建立 bject3被回收 bject2被回收 bject1被回收

results matching ""

    No results matching ""