Java學習之路08—方法


架構圖

方法簡介

甚麼是方法

方法就是用來解決特殊需求的一個功能模塊、程式語句的集合,把需要重複使用的功能包裝到方法裡面。例如我們最常使用的System.out.println(),就是調用System類中實例化的對象out中的方法println()

在java中方法一定存在於某個類之中,這點跟C/C++, Python有點不同

方法格式

一個方法的編寫可以參考下列格式:

修飾符 返回值類型 方法名(數據類型1 參數名1, 數據類型2 參數名2){

    // 方法主體

    return 返回值;
}
  1. 修飾符
    • 告訴compiler如何調用該方法(option)
    • 例如封裝性的public, private,靜態方法的static
  2. 返回類型
    • 決定方法執行完成後的返回值數據類型
    • 例如基本數據類型與引用數據類型
    • void類型則無返回值
  3. 方法名
    • 方法的名稱,通常有約定俗成的規範
  4. 參數列表
    • 調用該方法時需要傳入的數據類型與數量
    • 例如基本數據類型與引用數據類型
    • 也可以不傳入任何參數
  5. 方法體
    • 方法的程式邏輯區段
  6. 返回值
    • 方法的返回值
    • 若返回類型為void,則可以不填

舉例說明,下面是一個真實的方法案例:

方法的命名

方法的命名通常與變數一樣,採用小駝峰法也就是說當方法名為單一英文單字時要求小寫,若是兩個英文單字的組合,第一個單字需要小寫,第二個英文單字開頭大寫

另外兩種英文單字的組合通常是:

  1. 動詞縮寫
    • 例如get, set, update, remove
    • 目的: 為了完成指定動作
  2. 以動詞 + 名詞方式命名
    • 例如ensureCapacity, displayMenu, initializaTable
    • 目的: 為了完成指定動作,並指名操作目標
  3. 助動詞 + 動詞/名詞
    • 例如shouldMigrate, canInput, mayDump
    • 目的: 通常涉及到使用時機的動作
  4. 使用be動詞開頭 + 名詞/形容詞
    • 例如isValid, isOpen, isFinish
    • 目的: 用來判斷當前的行為是否合法
  5. 用來表達長度、容量、大小可以使用名詞
    • 例如length, size, width
    • 目的: 用來獲取、運算指定參數屬性

不過說到底這些命名規則共同目標都是為了讓開發者能快速了解方法的作用,太簡潔不易理解;太詳細容易陷入冗長,該怎麼拿捏還是要看團隊的共識,如果想更了解命名規則的話可以參考這篇

方法的調用

方法的調用主要取決於是否有返回值,以及方法位置

若方法調用具有返回值,則該方法實際上被視為一個數值,當控制權藉由方法返回給調用語句時,通常會將返回值賦值給一個左值變數(也可以選擇不進行賦值)

返回值類型void則代表無返回值類型,調用時直接使用,不需要進行賦值配置,因為它實際上就是一條語句

另外主方法main也是一個返回類型void的方法,本質上它與其他方法沒有任何差異,只不過它是由JVM調用,代表程式的入口,main方法結束後整個程式便結束了,所以不需要回傳任何數值

下面程式範例介紹方法的調用方式:

public class Main
{
    public static int testOne(int num){
        return num + 10;
    }

    public static void testTwo(int num){
        num++;
    }

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

        sum = testOne(sum); // 有返回值
        testOne(sum); // 有返回值
        testTwo(sum); // 無返回值
    }
}

調用的方法若位於不同的類當中,則需要先創建對象,實例化後再進行調用。相同類中的方法可以直接調用,但須要注意static方法只能調用同類中的static方法(後面章節會提到)

主方法中若想調用其他類中的方法,需要先實例化操作:

public class Main
{
    public static void read(){
        System.out.println("class Main read");
    }

    public static void main(String[] args) {
        Student stu = new Student();
        stu.read();
        read();
    }
}

Student類中的read方法:

public class Student{
    public void read(){
        System.out.println("class Student read");
    }
}

方法的結束
有開始必定有結束,一個方法將控制權返回給調用方通常會滿足下面三個條件之一:

  1. 方法執行完所有的語句
  2. 執行return
  3. 拋出異常

scope

scope就是在程式執行區段中一個變數可以被引用的範圍。一個變數的作用範圍可以藉由修飾符與所處程式碼區段,在編譯時決定

而在方法中我們使用的變數類型為局部變數,它儲存在棧空間中(stack)。一個方法內的局部變數在調用方法時生成,返回時銷毀。在方法體內我們可以使用{}另闢一個程式碼區塊,不同區塊中可以宣告同名的局部變數,因為他們的scope範圍各自在退出括號後結束,因此不衝突,例如:

方法中在執行的過程中維護了一個稱為局部變數表的表格,如果該變數的執行區段一結束,該變數就會從該表中刪除。相反的,若一個變數還未被刪除,就不允許再宣告一個同名變數

在方法test中宣告的temp還未被釋放就宣告一個同名的變數,會產生重複宣告的錯誤,例如下面的例子:

方法多載

多載簡介

多載(overloading)就是一個類當中具有多個同名的方法,但會具有以下差異:

  1. 傳入的參數類型不同
  2. 參數列表數量不同
  3. 參數順序不同

換句話說就是兩個方法的方法簽名(Method Signature)不同

多載方法適用於功能相同但是輸入參數不同的情況。也就是說會調用哪個多載方法,需要看使用者傳入的引數(Argument)決定,多載能夠讓開發者不用為了參數不同再去創建一個具有相似功能但名稱不同的方法

例如以下程式碼範例,主方法調用的addSum方法會依照上述所說的三種差異而使用不同的多載方法:

public class Main{
    public static int addSum(int num1, int num2){
        System.out.print("方法一: ");
        return num1 + num2;
    }

    public static int addSum(int num1, int num2, int num3){
        System.out.print("方法二: ");
        return num1 + num2 + num3;
    }

    public static double addSum(double num1, double num2, double num3){
        System.out.print("方法三: ");
        return num1 + num2 + num3;
    }

    public static double addSum(double num1, int num2){
        System.out.print("方法四: ");
        return num1 + num2;
    }

    public static double addSum(int num1, double num2){
        System.out.print("方法五: ");
        return num1 + num2;
    }

    public static void main(String[] args) {
        System.out.println("sum = " + addSum(3, 1));

        /*參數數量不同*/
        System.out.println("sum = " + addSum(3.5, 1.2, 10.0));

        /*參數類型不同*/
        System.out.println("sum = " + addSum(3, 1, 10));

        /*順序不同*/
        System.out.println("sum = " + addSum(3, 1.2));
        System.out.println("sum = " + addSum(3.5, 1));
    }
}

輸出結果:

方法一: sum = 4
方法三: sum = 14.7
方法二: sum = 14
方法五: sum = 4.2
方法四: sum = 4.5

編寫多載方法的注意事項

  1. 方法名一定要一樣
  2. 主要不同是參數類型以及參數數量
  3. 只有參數名不同不算是多載
  4. 只有返回數據類型不同不算多載
  5. 返回值可以不同,也包括void無返回值類型

下面這段程式碼會依照使用者傳入引數來決定各種形狀的面積,剛好就是一種無返回值的多載實現:

public class Main
{
    public void area(double radius){
        System.out.println("圓的面積為:" + (radius * radius * Math.PI));
    }

    public double area(double radius){
        System.out.println("圓的面積為:" + (radius * radius * Math.PI));
        return radius * radius * Math.PI
    }

    public void area(float length, float width){
        System.out.println("長方形面積為:" + (length * width));
    }

    public void area(double top, double bottom, double high){
        System.out.println("梯形面積為:"+(top + bottom)*high/2);
    }

    public static void main(String[] args) {
        Main m = new Main();
        double r=3.9;
        float l=4, w=7;
        double t=3.0, b=11.2, h=4.3;

        m.area(r);
        m.area(l, w);
        m.area(t, b, h);
    }
}

但是就如同我們上面提到的,假若兩個多載方法如果只有返回數據類型不同,不能算是多載的一種

這時候編譯會產生error: method 方法簽名 is already defined in class 類,代表系統將這兩個方法視為同一個方法,發生重定義錯誤。例如我們在上一個程式碼範例中添加一個圓面積的多載方法:

public class Main
{
    public void area(double radius){
        System.out.println("圓的面積為:" + (radius * radius * Math.PI));
    }

    /*改變返回類型*/
    public double area(double radius){
        System.out.println("圓的面積為:" + (radius * radius * Math.PI));
        return radius * radius * Math.PI;
    }

    public void area(float length, float width){
        System.out.println("長方形面積為:" + (length * width));
    }

    public void area(double top, double bottom, double high){
        System.out.println("梯形面積為:"+(top + bottom)*high/2);
    }

    public static void main(String[] args) {
        Main m = new Main();
        double r=3.9;
        float l=4, w=7;
        double t=3.0, b=11.2, h=4.3;

        m.area(r);
        m.area(l, w);
        m.area(t, b, h);
    }
}

輸出結果:

Main.java:15: error: method area(double) is already defined in class Main
    public double area(double radius){
                  ^
1 error

💡 至於void與其他數據類型返回值可不可以多載使用?答案當然是可以,只不過多載的中心思想是功能類似。舉上面的void返回類型方法來說,方法就是透過打印輸出來實現,如果我們要多加一個float返回類型的方法(返回面積),那這個多載方法事實上已經與其他同名的方法功能上有所不同了,這與多載的核心思想背道而馳

引數無匹配狀況

有時候調用方法傳入的引數不一定與類中的所有多載方法匹配,一般來說編譯時會發出incompatible types的錯誤。但如果傳入引數可以類型轉換成符合多載方法的參數類型,那麼編譯器還是可以正常調用方法。舉例來說:

public class Main
{
    public static void test(float num){
        System.out.println("float類型方法");
    }

    public static void test(double num){
        System.out.println("double類型方法");
    }

    public static void main(String[] args) {
        int i=10;
        byte b=5; 

        test(i); // 調用方法簽名為test(float)的方法
        test(b); // 調用方法簽名為test(float)的方法
    }
}

輸出結果:

float類型方法
float類型方法

透過上述例子,我們發現在方法調用時會依照以下幾個步驟找尋目標多載方法:

  1. 會依照完整參數類型來尋找
  2. 會依照數據類型轉換表方式尋找符合更高階的參數類型
  3. 能用來完整表示傳入引數的最小長度參數類型

我們再把上述程式碼加上幾個參數類型與多載方法:

public class Main
{
    /*新增test(int)方法*/
    public static void test(int num){
        System.out.println("int類型方法");
    }

    public static void test(float num){
        System.out.println("float類型方法");

    }

    public static void test(double num){
        System.out.println("double類型方法");
    }

    public static void main(String[] args) {
        int i=10;
        byte b=5;
        long l=100l; // long類型

        test(i); // 找到符合參數列表方法
        test(b); // 類型轉換成int類型
        test(l); // 會調用test(float)而不是test(double)
    }
}

輸出結果:

int類型方法
int類型方法
float類型方法

呼叫

在java中當呼叫方法將引數傳遞給參數時主要有兩種方式:

  1. call by value(傳值呼叫)
  2. call by reference(傳參考呼叫)

為了理解這兩種呼叫方法,我們需要特別觀察實際參數(Actual Parameter)與形式參數(Formal Parameter)之間的關係和變化,例如下方範例程式碼的num1就是實際參數,而num2為形式參數

換句話說,我們要觀察一個變數值會不會因為調用方法而改變

public class Main
{
    public static void test(int num2){ // 形式參數
        num2 += 10;
        System.out.println("In method num = " + num2);
    }

    public static void main(String[] args) {
        int num1=51; // 實際參數
        System.out.println("Before num = " + num1);
        test(num);
        System.out.println("Afer num = " + num1);
    }
}

傳值呼叫

當方法調用時,不管形式參數如何變動最後都不會影響到實際參數的值,這種呼叫方式稱為傳值呼叫。通常使用基本數據類型當作引數調用方法,都是使用傳值呼叫

其實以下程式碼範例就是一個傳值呼叫案例,num1的值在調用完後依然保持不變:

public class Main
{
    public static void test(int num2){
        num2 += 10;
        System.out.println("In method num = " + num2);
    }

    public static void main(String[] args) {
        int num1=51;
        System.out.println("Before num = " + num1);
        test(num);
        System.out.println("Afer num = " + num1);
    }
}

輸出結果:

Before num = 51
In method num = 61
Afer num = 51

我們先假設num1儲存在地址0x0001的位置,當num1作為引數傳入給方法test時,該方法會在棧空間中(stack)創建一個幀(frame),這個幀包含了參數值、程式碼區段、局部變數與返回地址等

其中形式參數num2(地址0x0020)會儲存num1複製過來的變數值,也就是說形式參數與實際參數雖然儲存的數值相同但它們分別位於棧空間中的不同位置。而方法一旦返回,原先創建的記憶體空間就會自動被銷毀,所以到頭來num2值的變化僅存在於方法的scope當中,不會對num1有任何影響

傳參考呼叫

當方法調用時,形式參數經過修改最後會影響到實際參數的值,這種呼叫方式稱為傳參考呼叫。通常以物件、陣列、字串等引用數據類型的變數當作引數調用方法,都是使用傳參考呼叫

例如以下程式碼範例,陣列arr經過方法array調用後,其元素會被改變:

public class Main
{
    public void print(int []arr){
        System.out.print("arr = ");
        for(int i=0; i<arr.length; i++){
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    public void array(int []arr, int num){
        arr[num] = 100;
        print(arr);
    }

    public static void main(String[] args) {
        Main ad = new Main();
        int []arr = {1,2,3,4,5,6,7,8,9};
        System.out.print("Before ");
        ad.print(arr);
        System.out.print("In method  ");
        ad.array(arr, 0); // 將index為0的元素改為100
        System.out.print("After ");
        ad.print(arr);
    }
}

輸出結果:

Before arr = 1 2 3 4 5 6 7 8 9 
In method  arr = 100 2 3 4 5 6 7 8 9 
After arr = 100 2 3 4 5 6 7 8 9 

會產生這種狀況是因為傳入的arr儲存的其實是一個參考物件的地址。經過Main ad = new Main();語句在堆空間中(heap)生成對象,然後將arr參考到該對象

因此我們調用方法時會將arr的內容,也就是該對象的地址複製給方法參數中的參考變數arr。所以不管是主方法中的arr,抑或是調用方法中的arr,都是參考到相同一個對象。這時其中一方修改該對象的內容,不管最終這個參考變數被銷毀,對象的內容都會被保留下來

傳遞參數

一般來說調用方法時,我們會依照方法的參數列表依序傳入引數。如同之前介紹,若是存在多載,則會依據參數列表決定多載方法。方法參數一般來說需要符合方法簽名定義的格式。不過也是有例外的,例如我們提過的類型轉換

命令行參數

不要忘了入口main也是一個方法,它的返回數據類型為void,參數列表為一個字串陣列的方法。啟動程式時JVM會自動尋找main作為程式起點,一般情況下都是傳入一個空的字串物件

但為了瞭解main方法的實現,我們編寫下面的程式來捕捉傳入參數:

public static void main(String[] args) {
    for(int i=0; i<args.length; i++)
        System.out.println("args[" + i + "]= " + args[i]);
}

使用命令行方法傳入參數
首先我們將剛剛編寫的程式儲存成.java檔,然後開啟任何型式的命令行介面,先打上javac Test.java將.java檔編譯成.class文件

接者輸入java Test 參數1 參數2 參數3 ...執行.class文件,其中每個字串陣列元素以空白鍵相隔。輸入後查看運行結果,參數確實傳入給main方法

使用eclipse傳入參數
但話又說回來,IDE才是最普遍的使用場景,我們這裡演示使用eclipse來為main方法傳入參數

首先我先隨便開起一個package,在編輯頁面點擊右鍵 → Run As → Run Configurations:

選擇Arguments選單並在Program arguments欄位中依序填入參數,每個字串陣列以enter鍵區隔,設置完成後按下右下角的Run:

輸出結果與使用命令行相同:

不定長度引數

什麼是不定長度引數
不定參數引數(Variable-length Argument)是編譯器的語法糖,它允許調用方每次傳入不同的參數數量,其中也包括傳入0個參數。不定長度引數在參數列表中使用數據類型 ... 變數名來表示,調用方法時需傳入相同數據類型的引數,當然也可以傳入引用數據類型數據

我們常用的println方法也是一種常見的不定長度引數的應用:

/*println就是一種不定長度引數的實現*/
System.out.println(a);
System.out.println(a + " " + b);
System.out.println(a + " " + b + " " + c);

其實將型式參數數據類型 ... 變數名展開後,它就是一個一維陣列。而我們在調用該方法時,同樣也是向方法傳入一個一維陣列。這也代表我們可向使用不定長度引數的方法傳入具有相同數據類型的陣列

public class Main{
    public static void test (int ... num){
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }

    public static void main (String[] args){
        int[] arr = {1,2,3,4,5};

        test(5,4,3,2,1);
        System.out.println();
        test(arr);// 傳入陣列
    }
}

輸出結果:

num[0] = 5
num[1] = 4
num[2] = 3
num[3] = 2
num[4] = 1

num[0] = 1
num[1] = 2
num[2] = 3
num[3] = 4
num[4] = 5

使用格式注意

  1. 當與其他參數搭配時,一定要將不定長度引數放在最後面
public static void test (float f, String[] str, int ... num) // 正確
public static void test (float f str, int ... num, String[]) // 錯誤
  1. 不能同時使用兩個不定長度引數
public static void test (int ... num, String ... str) // 不允許同時使用兩個可變長度引數
  1. 數據類型均相同
    • 方法設定的不定長度引數數據類型是甚麼,調用方法時就應該傳入相同數據類型的引數
public class Main{
    public static void test (float ... num){
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }

    public static void main (String[] args){      
        test(5.0f,4.0f,3.0f,2.0f,1.0f); // 均傳入float,可以視為一個float類型的陣列
    }
}
  1. 陣列可做為參數傳給不定長度引數,但相反不成立

多載特性
不定長度引數通常配合多載一起使用,不過通常會伴隨模糊性產生。這主要是因為多載時不定長度引數會與陣列視為相同數據類型的參數,因此會報錯,例如以下程式碼:

public class Main{
    public static void test (int ... num){
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }

    public static void test (int[] num){
        for(int i: num){
            System.out.println("num = " + i);
        }
    }

    public static void main (String[] args){
        test(5,4,3);
    }
}

輸出結果:

Main.java:22: error: cannot declare both test(int[]) and test(int...) in Main
    public static void test (int[] num){
                       ^
1 error

訪問優先級
不定長度引數是最後被訪問的,若有其他方法符合調用時的引數數量與類型,則優先訪問,例如以下程式碼:

public class Main{
    public static void test (int ... num){
        System.out.println("Variable-length Argument");
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }

    public static void test (int num1, int num2, int num3){
        System.out.println("regular method");
        System.out.println("num1 = " + num1);
        System.out.println("num2 = " + num2);
        System.out.println("num3 = " + num3);        
    }

    public static void main (String[] args){
        test(5,4,3);
    }
}

輸出結果:

regular method
num1 = 5
num2 = 4
num3 = 3

轉型問題
使用不定長度引數時,若傳入引數找不到完全相同數據類型的方法,可以退而求其次,使用轉型來匹配方法,不過需要滿足類型轉換表的轉換順序

舉以下程式碼為例,我們在主方法中調用方法test,傳入引數類型為char,但實際類中並沒有參數類型為char的方法,所以編譯器會將引數經過類型轉換來匹配方法

這個範例中數據類型charintdouble依序轉換,因此成功調用不定參數類型為double的方法。另外,若是不定長度引數與其他參數組合一同出現時,類型轉換依然成立

public class Main {

    public void test(double ... num){
        System.out.println("方法1");
        for(double n:num){
            System.out.print(n + " ");
        }
        System.out.println();
    }

    public void test(char[] arr, double ... words){ // 不定長度引數與其他參數組合一起出現
        System.out.println("方法2");
        for(char n:arr){
            System.out.print(n + " ");
        }
        System.out.println();
        for(double n:words){
            System.out.print(n + " ");
        }
    }

    public static void main(String[] args) {
        Main md = new Main();
        char[] arr = {'a', 'p', 'p', 'l', 'e'};

        md.test('b','a'); // 轉換為double類型
        System.out.println("===============");
        md.test(arr, 'b','a'); // 轉換為char[]與double類型
    }
}

輸出結果:

方法1
98.0 97.0 
===============
方法2
a p p l e 
98.0 97.0 

若引數是傳入陣列類型則不適用類型轉換,例如方法中的不定長度引數為float類型,但陣列傳入int類型,編譯器會報錯int[]無法轉換成float[]類型,因為引用數據類型之間無法使用類型轉換

例如剛剛的範例程式碼,我們使用字元陣列調用test,看是否能達到不定長度引數的效果:

public class Main {

    public void test(double ... num){
        System.out.println("方法1");
        for(double n:num){
            System.out.print(n + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Main md = new Main();
        char[] arr = {'a', 'p', 'p', 'l', 'e'};

        md.test(arr);
    }
}

輸出結果:

Main.java:34: error: method test in class Main cannot be applied to given types;
        md.test(arr);
          ^
  required: double[]
  found: char[]
  reason: varargs mismatch; char[] cannot be converted to double
1 error

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

%d 位部落客按了讚: