計算機程序的思惟邏輯 (11) - 初識函數

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

函數

前面幾節咱們介紹了數據的基本類型、基本操做和流程控制,使用這些已經能夠寫很多程序了。java

可是若是須要常常作某一個操做,則相似的代碼須要重複寫不少遍,好比在一個數組中查找某個數,第一次查找一個數,第二次可能查找另外一個數,每查一個數,相似的代碼都須要重寫一遍,很羅嗦。另外,有一些複雜的操做,可能分爲不少個步驟,若是都放在一塊兒,則代碼難以理解和維護。編程

計算機程序使用函數這個概念來解決這個問題,即使用函數來減小重複代碼和分解複雜操做,本節咱們就來談談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,它的目的是在屏幕上輸出三個空行,它沒有輸入參數,操做是使用一個循環輸出三個空行,它沒有返回值。函數

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

修飾符 返回值類型  函數名字(參數類型 參數名字, ...) {
    操做 ...
    return 返回值;
}
複製代碼

函數的主要組成部分有:spa

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

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

函數調用

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庫中就定義了四個函數,以下所示:

public static double max(double a, double b) public static float max(float a, float b) public static int max(int a, int b) public static long max(long a, long b) 複製代碼

調用過程

匹配過程

在以前介紹函數調用的時候,咱們沒有特別說明參數的類型。這裏說明一下,參數傳遞其實是給參數賦值,調用者傳遞的數據須要與函數聲明的參數類型是匹配的,但不要求徹底同樣。什麼意思呢?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,理解這個錯誤,咱們須要理解函數調用的實現機制,讓咱們下節介紹。


更多文章

計算機程序的思惟邏輯 (5) - 小數計算爲何會出錯?

計算機程序的思惟邏輯 (6) - 如何從亂碼中恢復 (上)?

計算機程序的思惟邏輯 (7) - 如何從亂碼中恢復 (下)?

計算機程序的思惟邏輯 (8) - char的真正含義

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

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


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。原創文章,保留全部版權。

相關文章
相關標籤/搜索