本系列文章經補充和完善,已修訂整理成書《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
以上就是定義函數的語法,定義函數就是定義了一段有着明確功能的子程序,但定義函數自己不會執行任何代碼,函數要被執行,須要被調用。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的含義。函數返回值類型爲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,理解這個錯誤,咱們須要理解函數調用的實現機制,讓咱們下節介紹。
更多文章
計算機程序的思惟邏輯 (6) - 如何從亂碼中恢復 (上)?
計算機程序的思惟邏輯 (7) - 如何從亂碼中恢復 (下)?
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。原創文章,保留全部版權。