计算机程序的思惟逻辑 (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编程及计算机技术的本质。原创文章,保留全部版权。

相关文章
相关标签/搜索