National Yang Ming Chiao Tung University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

陽明交大 AQI 69 24°C PM2.5 26
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 07

方法

參數與回傳值、多載、遞迴與傳值呼叫
Interaction實作 25 minute(s)

Learning Objectives

  • 定義與呼叫 static 方法,分辨參數與引數,說明回傳值與 void 的差別
  • 用 return 提早結束方法,說明區域變數只在自己的方法裡有效
  • 說明 Java 一律傳值:傳基本型別改不到呼叫端的變數,傳陣列則可以改到陣列內容
  • 依照參數列寫出多載(overload)方法,知道只有回傳型別不同不算多載;使用可變長度參數
  • 寫出有終止條件的遞迴方法,用呼叫堆疊說明遞迴怎麼一層一層返回

為什麼要寫方法

程式寫長了,常會發現同一段計算出現在好幾個地方,或是 main 長到一眼看不完。方法(method)把一段工作包起來、取一個名字, 需要的時候呼叫它就好。你其實已經用過很多方法:System.out.println(…)、Math.sqrt(…)、Arrays.sort(…)。

把程式拆成方法有三個好處:

  • 不重複:同樣的計算只寫一次,要修正也只改一個地方
  • 好讀:if (isPrime(n)) 比一整段迴圈更容易看懂意圖
  • 好測試:每個方法負責一件事,可以單獨確認它是否正確

本課的方法都加上 static,表示它屬於 Main 這個類別,在 main 裡可以直接呼叫。 沒有 static 的方法要先建立物件才能呼叫,這是第 08 課的內容。

學過 Python 的話:Java 的方法相當於 Python 的 def,但每個參數和回傳值都要寫出型別,編譯器會檢查你傳的引數和接收的回傳值型別對不對。

定義與呼叫

static int square(int n) {   // 回傳型別 int、方法名稱 square、參數 int n
    int result = n * n;
    return result;           // 把結果交回呼叫的地方
}

int a = square(3);           // 呼叫:引數 3 複製給參數 n,回傳值 9 存進 a
部分 說明
static 屬於類別的方法(本課的方法都要加)
回傳型別 方法算完交回的值是什麼型別;不回傳任何值就寫 void
方法名稱 習慣用小寫開頭的駝峰式(camelCase),通常是動詞:printReport、isPrime、getTotal
參數列 小括號裡的 型別 名稱,用逗號分隔;沒有參數也要寫 ()
  • 定義時小括號裡的 n 叫參數(parameter);呼叫時實際傳進去的 3 叫引數(argument)。引數的個數、順序、型別要和參數列對得上
  • 呼叫方法時,程式跳進方法裡執行,碰到 return(或 void 方法的結尾)再回到呼叫的地方繼續
  • 有回傳值的方法可以放進任何運算式:square(2) * square(5)、System.out.println(square(4))
  • void 方法沒有回傳值,寫 int r = greet("Amy", 2); 會編譯錯誤

每呼叫一次方法,動畫左邊就多一個框架(frame),放這次呼叫的參數與區域變數;方法結束,框架就消失。這一疊框架叫做呼叫堆疊(call stack)。

呼叫方法、拿回回傳值

每次呼叫 square,左邊就疊上一個新的框架,裡面的 n 是這次傳進來的引數;回傳步驟會在框架上顯示回傳值,接著框架消失,回到 main。注意 greet 的參數有兩個,引數 "Amy" 和 2 依照順序對應到 name 和 times。

return 與區域變數

return 會立刻結束方法,後面的程式不再執行。所以一個方法可以有好幾個 return,先把特殊情況處理掉:

static String grade(int score) {
    if (score < 0 || score > 100) {
        return "分數不合理";   // 不合理的輸入,直接結束
    }
    if (score >= 80) return "A";
    if (score >= 60) return "B";
    return "C";
}
  • 有回傳型別的方法,每一條可能的執行路徑都必須 return 一個值,否則編譯錯誤 missing return statement
  • void 方法也可以寫不帶值的 return; 提早結束
  • 在迴圈裡 return 會連同迴圈一起結束整個方法,常用在「找到答案就停」,例如判斷質數時找到一個因數就回傳 false

區域變數(local variable):在方法裡宣告的變數(包括參數)只屬於這次呼叫,方法結束就消失。 main 不能直接使用 square 裡的 result,兩個方法裡也可以各有一個同名的變數,彼此互不相干。 需要把結果交給呼叫端,就用回傳值。

提早 return

grade(85) 在第 8 行就 return,第 10 行之後不會執行;grade(120) 更早,在第 5 行就結束。isPrime(9) 在迴圈裡找到因數 3 就直接 return,迴圈不會跑完。

想一想:呼叫、回傳值與範圍

1.

下面的程式會印出什麼?

public class Main {
    static void p(String s) { System.out.print(s); }
    static int twice(int n) { p("t"); return n * 2; }

    public static void main(String[] args) {
        int v = twice(twice(3));
        System.out.println(" " + v);
    }
}
2.

下面的程式會印出什麼?

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

    public static void main(String[] args) {
        int r = hello();
    }
}
3.

下面的程式會印出什麼?

public class Main {
    static void swap(int a, int b) {
        int t = a;
        a = b;
        b = t;
    }

    public static void main(String[] args) {
        int x = 1, y = 2;
        swap(x, y);
        System.out.println(x + " " + y);
    }
}
第 1 次作答

傳值呼叫

Java 呼叫方法時一律傳值(pass by value):把引數的值複製一份給參數。差別在於「值」是什麼:

引數的型別 複製的是 方法裡修改參數的結果
基本型別(int、double…) 數值本身 只改到複本,呼叫端的變數不變
陣列(以及第 08 課的物件) 參照(指向同一個陣列) 改元素:呼叫端看得到;讓參數改指向新陣列:呼叫端不受影響

所以 swap(x, y) 交換不了兩個 int 變數。要讓方法的計算結果影響呼叫端,最清楚的做法是用回傳值:num = addOne(num);。 方法直接修改傳進來的陣列雖然可行,但呼叫的人不一定想得到,最好在方法名稱或註解裡說清楚(例如 Arrays.sort 就是直接改原陣列)。

基本型別與陣列的差別

呼叫 change(num, data) 時,新框架的 x 是 5 的複本,arr 的箭頭則指向 main 的同一個陣列。第 6 行改 x,只有 change 框架裡的值變了;第 7 行改 arr[0],右邊那個陣列真的變了。回到 main 後比較 num 和 data。

多載與可變長度參數

同一個類別裡可以有好幾個同名的方法,只要參數列不同(參數的個數不同,或對應位置的型別不同),這叫做多載(overload)。 System.out.println 就是多載的例子:println(int)、println(double)、println(String) 都叫同一個名字。

static int max(int a, int b) { … }
static double max(double a, double b) { … }   // 型別不同:可以
static int max(int a, int b, int c) { … }     // 個數不同:可以
static double max(int a, int b) { … }         // 只有回傳型別不同:編譯錯誤

呼叫時,編譯器依照引數的型別與個數選出要用哪一個。只有回傳型別不同不算多載,因為呼叫 max(3, 8) 時看不出你要哪一個, javac 會報 method max(int,int) is already defined。參數的名稱不同也不算,只看型別。 找不到完全相符的版本時,編譯器會嘗試把引數自動放寬(例如 int 轉成 long 或 double)再找。

可變長度參數(varargs):在最後一個參數的型別後面加 ...,方法就能接受 0 個或任意多個引數:

static int sum(int... nums) {   // 在方法裡,nums 就是一個 int[]
    int total = 0;
    for (int n : nums) total += n;
    return total;
}

sum();          // nums 是長度 0 的陣列
sum(1, 2, 3);   // nums 是 {1, 2, 3}

一個方法只能有一個可變長度參數,而且必須放在參數列的最後:static String join(String sep, String... words)。

編譯器依引數挑選版本

看每次呼叫時左邊框架的名稱:max(int a, int b)、max(double a, double b)、max(int a, int b, int c) 是三個不同的方法。三個參數的版本自己又呼叫兩次兩個參數的版本。最後的 sum 每次呼叫,右邊都會出現一個新的 int[],就是可變長度參數收到的引數。

遞迴

方法可以呼叫自己,這叫遞迴(recursion)。有些問題可以拆成「一個比較小的同類問題」,用遞迴寫起來特別直接。 例如階乘:4! = 4 × 3!,而 3! = 3 × 2!……直到 1! = 1。

static int factorial(int n) {
    if (n <= 1) {
        return 1;                  // 終止條件(base case):不再呼叫自己
    }
    return n * factorial(n - 1);   // 遞迴呼叫:問題變小一點
}

一個正確的遞迴方法一定要有兩個部分:

  1. 終止條件:最簡單的情況直接回傳答案
  2. 遞迴呼叫:每次呼叫都讓問題更接近終止條件

每次呼叫都會在呼叫堆疊上疊一個新框架,每個框架有自己的 n。碰到終止條件之後,才從最上層開始一層一層返回、把結果乘回來。 如果忘了終止條件,或問題沒有變小,框架會一直疊下去,最後拋出 StackOverflowError。

費氏數列 fib(n) = fib(n - 1) + fib(n - 2) 也可以直接寫成遞迴,但同一個值會被重複計算很多次(下面的動畫裡 fib(2) 算了兩次), n 一大就非常慢。實務上這類問題改用迴圈或把算過的結果記下來。遞迴適合用在問題本身就是樹狀、可以自然拆小的情況。

呼叫堆疊上的遞迴

factorial(4) 會在左邊疊出四層 factorial(int n) 框架,每一層的 n 都不同。到 n 是 1 時碰到終止條件,接著連續幾步都是回傳:每一層拿到上一層的結果,乘上自己的 n 再交回去。後半段的 fib(4) 呼叫次數比較多,注意 fib(2) 被計算了兩次。

想一想:多載、可變長度參數與遞迴

1.

下面的程式會印出什麼?

public class Main {
    static int f(int x) { return x; }
    static double f(int x) { return x * 1.5; }

    public static void main(String[] args) {
        System.out.println(f(2));
    }
}
2.

下面的程式會印出什麼?

public class Main {
    static void show(int x) { System.out.print("int "); }
    static void show(double x) { System.out.print("double "); }

    public static void main(String[] args) {
        show(5);
        show(5L);
        show('A');
    }
}
3.

下面的程式會印出什麼?

public class Main {
    static int f(int n) {
        if (n == 0) return 0;
        return n + f(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(f(4));
    }
}
4.

下面的程式會印出什麼?

public class Main {
    static int count(String s, char... cs) {
        return s.length() + cs.length;
    }

    public static void main(String[] args) {
        System.out.println(count("ab") + " " + count("ab", 'x', 'y'));
    }
}
第 1 次作答

方法的命名與單一職責

寫方法時,可以用下面幾點檢查:

  • 名稱說出它做什麼:動詞開頭(printReport、computeTax),回傳 boolean 的方法用 is/has/can 開頭(isPrime、hasNext),讀起來像一句話:if (isLeapYear(y))
  • 一個方法只做一件事(單一職責):名稱裡出現「和」(例如 readAndPrintAndSave)通常表示該拆開。負責「計算」的方法回傳結果,負責「顯示」的方法再去印,比在計算方法裡直接 println 更容易重複使用
  • 參數不要太多:超過四、五個時,可以想想是不是該整理成一個類別(第 08 課)
  • 長度適中:一個方法超過一個畫面,通常可以再拆出幾個小方法

下面的練習就是這樣:isLeapYear 只負責判斷一年,countLeapYears 呼叫它來計數。

動手寫:閏年

完成兩個方法。isLeapYear(year):能被 4 整除但不能被 100 整除,或能被 400 整除的年份是閏年。countLeapYears(from, to):計算 from 到 to(包含兩端)之間有幾個閏年,請呼叫 isLeapYear 來判斷。

你的輸出
預期輸出

重點回顧

  • 方法的寫法:static 回傳型別 名稱(參數列) { … };不回傳值用 void;呼叫時引數依照順序複製給參數
  • return 立刻結束方法;有回傳型別的方法每條路徑都要回傳值
  • 參數與方法裡宣告的變數是區域變數,只在這次呼叫裡有效;每次呼叫都有自己的框架,疊成呼叫堆疊
  • Java 一律傳值:基本型別傳數值的複本,改不到呼叫端;陣列傳參照的複本,改元素呼叫端看得到。想改變呼叫端的變數,用回傳值接回來
  • 多載:同名、參數列不同(個數或型別);只有回傳型別或參數名稱不同不算
  • 可變長度參數 int... nums 在方法裡是陣列,必須放在參數列最後
  • 遞迴要有終止條件,而且每次呼叫都讓問題變小;否則會 StackOverflowError
  • 方法名稱說清楚做什麼,一個方法只做一件事
這一課完成了!
Next lesson:物件與類別
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。