Java SE 6 技術手冊

7.2 關於方法

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

7.2.1 重載(Overload)方法

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

一個例子可以從 String 類別上提供的一些方法看到,像是 String 的 valueOf() 方法就提供了多個版本:

static String valueOf(boolean b)
static String valueOf(char c)
static String valueOf(char[] data)
static String valueOf(char[] data, int offset, int count)
static String valueOf(double d)
static String valueOf(float f)
static String valueOf(int i)
static String valueOf(long l)
static String valueOf(Object obj)

雖然您呼叫的方法名稱都是 valueOf(),但是根據所傳遞的引數資料型態不同,您會呼叫對應版本的方法來進行對應的動作,例如若是 String.valueOf(10),因為 10 是 int 型態,所以會執行的方法是 valueOf(int i) 的版本,而若是 String.valueOf(10.12),因為 10.12 是 double 型態,則會執行的方法是 valueOf(double d) 的版本。

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

在 J2SE 5.0 後當您使用方法重載時,要注意到 autoboxing、unboxing 的問題,來看看範例 7.9,您認為結果會是什麼?

範例 7.9 OverloadTest.java

public class OverloadTest {
    public static void main(String[] args) {
        someMethod(1);
    }

    public static void someMethod(int i) {
        System.out.println("int 版本被呼叫");
    }

    public static void someMethod(Integer integer) {
        System.out.println("Integer 版本被呼叫");
    }
}

結果會顯示 "int 版本被呼叫",您不能期待裝箱(boxing)的動作會發生,如果您想要呼叫參數列為 Integer 版本的方法,您要明確指定,例如:

someMethod(new Integer(1));

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

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

7.2.2 不定長度引數

在呼叫某個方法時,要給方法的引數個數事先無法決定的話該如何處理?例如 System.out.printf() 方法中並沒有辦法事先決定要給的引數個數,像是:

System.out.printf("%d", 10);
System.out.printf("%d %d", 10, 20);
System.out.printf("%d %d %d", 10, 20, 30);

在 J2SE 5.0 之後開始支援「不定長度引數」(Variable-length Argument),這可以讓您輕鬆的解決這個問題,直接來看範例 7.10 的示範。

範例 7.10 MathTool.java

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 的方式指定各種長度的引數給方法來使用。

範例 7.11 TestVarargs.java

public class TestVarargs {
    public static void main(String[] args) {
        int sum = 0;

        sum = MathTool.sum(1, 2);
        System.out.println("1 + 2 = " + sum);

        sum = MathTool.sum(1, 2, 3);
        System.out.println("1 + 2 + 3 = " + sum);

        sum = MathTool.sum(1, 2, 3, 4, 5);
        System.out.println("1 + 2 + 3+ 4+ 5 = " + sum);
    }
}

執行結果:

1 + 2 = 3
1 + 2 + 3 = 6
1 + 2 + 3+ 4+ 5 = 15

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

在方法上使用不定長度引數時,記得必須宣告的參數必須設定在參數列的最後一個,例如下面的方式是合法的:

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

但下面的方式是不合法的:

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

您也沒辦法使用兩個以上的不定長度引數,例如下面的方式是不合法的:

public void someMethod(int... varargs1, int... varargs2) {
     // ....
}

如果使用物件的不定長度引數,宣告的方法相同,例如:

public void someMethod(SomeClass... somes) {
     // ....
}

7.2.3 遞迴方法

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

範例 7.12 UseRecursion.java

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.12 是使用輾轉相除法來求最大公因數;遞迴具有重複執行的特性,而可以使用遞迴求解的程式,實際上也可以使用迴圈來求解,例如下面的程式片段就是最大公因數使用迴圈求解的方式。

private static int gcd(int m, int n) { 
    int r;
    while(n != 0) { 
        r = m % n; 
        m = n; 
        n = r; 
    }
    return m; 
} 

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

良葛格的話匣子 在我的網站上有很多題目可以作練習,也不乏有遞迴求解的例子:

7.2.4 垃圾收集

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

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

在 Java 中,使用 "new" 配置的物件,基本上也必須清除以回收物件所佔據的記憶體空間,但是您並不用特別關心這個問題,因為 Java 提供垃圾收集機制,在適當的時候,Java 執行環境會自動檢查物件,看看是否有未被參考的物件,如果有的話就清除物件、回收物件所佔據的記憶體空間。

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

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

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

沒有被名稱參考到的物件資源將會被回收

圖 7.5 沒有被名稱參考到的物件資源將會被回收

範例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被回收