電腦程式的思維邏輯 (11) - 初識函數

來源:互聯網
上載者:User

標籤:括弧   常量   地方   簡化   函數傳回值   進位   rac   示範   命名   

函數

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

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

電腦程式使用函數這個概念來解決這個問題,即使用函數來減少重複代碼和分解複雜操作,本節我們就來談談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!=1n!=(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,理解這個錯誤,我們需要理解函數調用的實現機制,讓我們下節介紹。

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

電腦程式的思維邏輯 (11) - 初識函數

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.