標籤:括弧 常量 地方 簡化 函數傳回值 進位 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) - 初識函數