本系列文章经补充和完善,已修订整理成书《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编程及计算机技术的本质。原创文章,保留全部版权。