Java編程的邏輯 (11) - 初識函數


本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營鏈接http://item.jd.com/12299018.html


函數

前面幾節我們介紹了數據的基本類型、基本操作和流程控制,使用這些已經可以寫不少程序了。

但是如果需要經常做某一個操作,則類似的代碼需要重復寫很多遍,比如在一個數組中查找某個數,第一次查找一個數,第二次可能查找另一個數,每查一個數,類似的代碼都需要重寫一遍,很羅嗦。另外,有一些復雜的操作,可能分為很多個步驟,如果都放在一起,則代碼難以理解和維護。

計算機程序使用函數這個概念來解決這個問題,即使用函數來減少重復代碼和分解復雜操作,本節我們就來談談Java中的函數,包括函數的基礎和一些細節。

定義函數

函數這個概念,我們學數學的時候都接觸過,其基本格式是 y = f(x),表示的是x到y的對應關系,給定輸入x,經過函數變換 f,輸出y。程序中的函數概念與其類似,也有輸入、操作、和輸出組成,但它表示的一段子程序,這個子程序有一個名字,表示它的目的(類比f),有零個或多個參數(類比x),有可能返回一個結果(類比y)。我們來看兩個簡單的例子:

public static int sum(int a, int b){
    int sum = a + b;
    return sum;
}

public static void print3Lines(){
    for(int i=0;i<3;i++){
        System.out.println();
    }
}

第一個函數名字叫做sum,它的目的是對輸入的兩個數求和,有兩個輸入參數,分別是int整數a和b,它的操作是對兩個數求和,求和結果放在變量sum中(這個sum和函數名字的sum沒有任何關系),然后使用return語句將結果返回,最開始的public static是函數的修飾符,我們后續介紹。

第二個函數名字叫做print3Lines,它的目的是在屏幕上輸出三個空行,它沒有輸入參數,操作是使用一個循環輸出三個空行,它沒有返回值。

以上代碼都比較簡單,主要是演示函數的基本語法結構,即:

修飾符 返回值類型  函數名字(參數類型 參數名字, ...) {
    操作 ...
    return 返回值;
}

函數的主要組成部分有:

  • 函數名字:名字是不可或缺的,表示函數的功能。
  • 參數:參數有0個到多個,每個參數有參數的數據類型和參數名字組成。
  • 操作:函數的具體操作代碼。
  • 返回值:函數可以沒有返回值,沒有的話返回值類型寫成void,有的話在函數代碼中必須要使用return語句返回一個值,這個值的類型需要和聲明的返回值類型一致。
  • 修飾符:Java中函數有很多修飾符,分別表示不同的目的,在本節我們假定修飾符為public static,且暫不討論這些修飾符的目的。

以上就是定義函數的語法,定義函數就是定義了一段有着明確功能的子程序,但定義函數本身不會執行任何代碼,函數要被執行,需要被調用。

函數調用

Java中,任何函數都需要放在一個類中,類我們還沒有介紹,我們暫時可以把類看做函數的一個容器,即函數放在類中,類中包括多個函數,Java中函數一般叫做方法,我們不特別區分函數和方法,可能會交替使用。一個類里面可以定義多個函數,類里面可以定義一個叫做main的函數,形式如:

public static void main(String[] args) {
      ...
}

這個函數有特殊的含義,表示程序的入口,String[] args表示從控制台接收到的參數,我們暫時可以忽略它。Java中運行一個程序的時候,需要指定一個定義了main函數的類,Java會尋找main函數,並從main函數開始執行。

剛開始學編程的人可能會誤以為程序從代碼的第一行開始執行,這是錯誤的,不管main函數定義在哪里,Java函數都會先找到它,然后從它的第一行開始執行。

main函數中除了可以定義變量,操作數據,還可以調用其它函數,如下所示:

public static void main(String[] args) {
    int a = 2;
    int b = 3;
    int sum = sum(a, b);

    System.out.println(sum);
    print3Lines();
    System.out.println(sum(3,4));
}

main函數首先定義了兩個變量 a和b,接着調用了函數sum,並將a和b傳遞給了sum函數,然后將sum的結果賦值給了變量sum。調用函數需要傳遞參數並處理返回值。

這里對於初學者需要注意的是,參數和返回值的名字是沒有特別含義的。調用者main中的參數名字a和b,和函數定義sum中的參數名字a和b只是碰巧一樣而 已,它們完全可以不一樣,而且名字之間沒有關系,sum函數中不能使用main函數中的名字,反之也一樣。調用者main中的sum變量和sum函數中的 sum變量的名字也是碰巧一樣而已,完全可以不一樣。另外,變量和函數可以取一樣的名字,但也是碰巧而已,名字一樣不代表有特別的含義。

調用函數如果沒有參數要傳遞,也要加括號(),如print3Lines()。

傳遞的參數不一定是個變量,可以是常量,也可以是某個運算表達式,可以是某個函數的返回結果。 如:System.out.println(sum(3,4)); 第一個函數調用 sum(3,4),傳遞的參數是常量3和4,第二個函數調用 System.out.println傳遞的參數是sum(3,4)的返回結果。

關於參數傳遞,簡單總結一下,定義函數時聲明參數,實際上就是定義變量,只是這些變量的值是未知的,調用函數時傳遞參數,實際上就是給函數中的變量賦值。

函數可以調用同一個類中的其他函數,也可以調用其他類中的函數,我們在前面幾節使用過輸出一個整數的二進制表示的函數,toBinaryString:

int a = 23;
System.out.println(Integer.toBinaryString(a));

toBinaryString是Integer類中修飾符為public static的函數,可以通過在前面加上類名和.直接調用。

函數基本小結

對於需要重復執行的代碼,可以定義函數,然后在需要的地方調用,這樣可以減少重復代碼。對於復雜的操作,可以將操作分為多個函數,會使得代碼更加易讀。

我 們在前面介紹過,程序執行基本上只有順序執行、條件執行和循環執行,但更完整的描述應該包括函數的調用過程。程序從main函數開始執行,碰到函數調用的時候,會跳轉進函數內部,函數調用了其他函數,會接着進入其他函數,函數返回后會繼續執行調用后面的語句,返回到main函數並且main函數沒有要執行的語句后程序結束。下節我們會更深入的介紹執行過程細節。

在Java中,函數在程序代碼中的位置和實際執行的順序是沒有關系的。

函數的定義和基本調用應該是比較容易理解的,但有很多細節可能令初學者困惑,包括參數傳遞、返回、函數命名、調用過程等,我們逐個討論下。

參數傳遞

數組參數

數組作為參數與基本類型是不一樣的,基本類型不會對調用者中的變量造成任何影響,但數組不是,在函數內修改數組中的元素會修改調用者中的數組內容。我們看個例子:

public static void reset(int[] arr){
    for(int i=0;i<arr.length;i++){
        arr[i] = i;
    }
}

public static void main(String[] args) {
    int[] arr = {10,20,30,40};
    reset(arr);
    for(int i=0;i<arr.length;i++){
        System.out.println(arr[i]);
    }
}

在reset函數內給參數數組元素賦值,在main函數中數組arr的值也會變。

這個其實也容易理解,我們在第二節介紹過,一個數組變量有兩塊空間,一塊用於存儲數組內容本身,另一塊用於存儲內容的位置,給數組變量賦值不會影響原有的數組內容本身,而只會讓數組變量指向一個不同的數組內容空間。

在上例中,函數參數中的數組變量arr和main函數中的數組變量arr存儲的都是相同的位置,而數組內容本身只有一份數據,所以,在reset中修改數組元素內容和在main中修改是完全一樣的。

可變長度的參數

上面介紹的函數,參數個數都是固定的,但有的時候,可能希望參數個數不是固定的,比如說求若干個數的最大值,可能是兩個,也可能是多個,Java支持可變長度的參數,如下例所示:

public static int max(int min, int ... a){
    int max = min;
    for(int i=0;i<a.length;i++){
        if(max<a[i]){
            max = a[i];
        }
    }
    return max;
}

public static void main(String[] args) {
    System.out.println(max(0));
    System.out.println(max(0,2));
    System.out.println(max(0,2,4));
    System.out.println(max(0,2,4,5));
}

這個max函數接受一個最小值,以及可變長度的若干參數,返回其中的最大值。可變長度參數的語法是在數據類型后面加三個點...,在函數內,可變長度參數可以看做就是數組,可變長度參數必須是參數列表中的最后一個參數,一個函數也只能有一個可變長度的參數。

可變長度參數實際上會轉換為數組參數,也就是說,函數聲明max(int min, int... a)實際上會轉換為 max(int min, int[] a),在main函數調用 max(0,2,4,5)的時候,實際上會轉換為調用 max(0, new int[]{2,4,5}),使用可變長度參數主要是簡化了代碼書寫。

返回

return的含義

對初學者,我們強調下return的含義。函數返回值類型為void且沒有return的情況下,會執行到函數結尾自動返回。return用於結束函數執行,返回調用方。

return可以用於函數內的任意地方,可以在函數結尾,也可以在中間,可以在if語句內,可以在for循環內,用於提前結束函數執行,返回調用方。

函數返回值類型為void也可以使用return,即return;,不用帶值,含義是返回調用方,只是沒有返回值而已。

返回值的個數

函數的返回值最多只能有一個,那如果實際情況需要多個返回值呢?比如說,計算一個整數數組中的最大的前三個數,需要返回三個結果。這個可以用數組作為返回值,在函數內創建一個包含三個元素的數組,然后將前三個結果賦給對應的數組元素。

如果實際情況需要的返回值是一種復合結果呢?比如說,查找一個字符數組中,所有重復出現的字符以及重復出現的次數。這個可以用對象作為返回值,我們在后續章節介紹類和對象。

我想說的是,雖然返回值最多只能有一個,但其實一個也夠了。

函數命名

每個函數都有一個名字,這個名字表示這個函數的意義,名字可以重復嗎?在不同的類里,答案是肯定的,在同一個類里,要看情況。

同一個類里,函數可以重名,但是參數不能一樣,一樣是指參數個數相同,每個位置的參數類型也一樣,但參數的名字不算,返回值類型也不算。換句話說,函數的唯一性標示是:類名_函數名_參數1類型_參數2類型_...參數n類型。

同一個類中函數名字相同但參數不同的現象,一般稱為函數重載。為什么需要函數重載呢?一般是因為函數想表達的意義是一樣的,但參數個數或類型不一樣。比如說,求兩個數的最大值,在Java的Math庫中就定義了四個函數,如下所示:

調用過程

匹配過程

在之前介紹函數調用的時候,我們沒有特別說明參數的類型。這里說明一下,參數傳遞實際上是給參數賦值,調用者傳遞的數據需要與函數聲明的參數類型是匹配的,但不要求完全一樣。什么意思呢?Java編譯器會自動進行類型轉換,並尋找最匹配的函數。比如說:

char a = 'a';
char b = 'b';
System.out.println(Math.max(a,b));

參數是字符類型的,但Math並沒有定義針對字符類型的max函數,我們之前說明,char其實是一個整數,Java會自動將char轉換為int,然后調用Math.max(int a, int b),屏幕會輸出整數結果98。

如果Math中沒有定義針對int類型的max函數呢?調用也會成功,會調用long類型的max函數,如果long也沒有呢?會調用float型的max函數,如果float也沒有,會調用double型的。Java編譯器會自動尋找最匹配的。
在只有一個函數的情況下(即沒有重載),只要可以進行類型轉換,就會調用該函數,在有函數重載的情況下,會調用最匹配的函數。

遞歸

函數大部分情況下都是被別的函數調用,但其實函數也可以調用它自己,調用自己的函數就叫遞歸函數。

為什么需要自己調用自己呢?我們來看一個例子,求一個數的階乘,數學中一個數n的階乘,表示為n!,它的值定義是這樣的:

0!=1
n!=(n-1)!×n

0的階乘是1,n的階乘的值是n-1的階乘的值乘以n,這個定義是一個遞歸的定義,為求n的值,需先求n-1的值,直到0,然后依次往回退。用遞歸表達的計算用遞歸函數容易實現,代碼如下:

public static long factorial(int n){
    if(n==0){
        return 1;
    }else{
        return n*factorial(n-1);
    }
}

看上去應該是比較容易理解的,和數學定義類似。

遞歸函數形式上往往比較簡單,但遞歸其實是有開銷的,而且使用不當,可以會出現意外的結果,比如說這個調用:

System.out.println(factorial(10000));

系統並不會給出任何結果,而會拋出異常,異常我們在后續章節介紹,此處理解為系統錯誤就可以了,異常類型為:java.lang.StackOverflowError,這是什么意思呢?這表示棧溢出錯誤,要理解這個錯誤,我們需要理解函數調用的實現原理(下節介紹)。

那如果遞歸不行怎么辦呢?遞歸函數經常可以轉換為非遞歸的形式,通過一些數據結構(后續章節介紹)以及循環來實現。比如,求階乘的例子,其非遞歸形式的定義是:

n!=1×2×3×…×n

這個可以用循環來實現,代碼如下:

public static long factorial(int n){
    long result = 1;
    for(int i=1; i<=n; i++){
        result*=i;
    }
    return result;
}

小結

函數是計算機程序的一種重要結構,通過函數來減少重復代碼,分解復雜操作是計算機程序的一種重要思維方式。本節我們介紹了函數的基礎概念,還有關於參數傳遞、返回值、重載、遞歸方面的一些細節。

但 在Java中,函數還有大量的修飾符, 如public, private, static, final, synchronized, abstract等,本文假定函數的修飾符都是public static,在后續文章中,我們再介紹這些修飾符。函數中還可以聲明異常,我們也留待后續文章介紹。

在介紹遞歸函數的時候,我們看到了一個系統錯誤,java.lang.StackOverflowError,理解這個錯誤,我們需要理解函數調用的實現機制,讓我們下節介紹。

----------------

未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。原創文章,保留所有版權。

-----------

更多相關原創文章

計算機程序的思維邏輯 (9) - 條件執行的本質

計算機程序的思維邏輯 (10) - 強大的循環

計算機程序的思維邏輯 (12) - 函數調用的基本原理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM