编程路上第一关:函数(方法)

根据我们的教学经验,很多0⃣️基础的同学在学习过程中往往遇到的第一个难点就是函数(也叫做方法)。他们往往在学习过程要么是根本不知道函数该如何自定义和调用;要么只能掌握语法却搞不清为啥要这么做;要么对于很多细节概念(形参、实参、返回值、方法调用栈)一窍不通,只能跟着感觉走🤷‍♂️。而函数在编程当中具有非常重要的地位,是以后进一步深入学习的重要基石,马虎不得!所以,今天我们就来帮大家梳理一下这编程路上第一关:函数关

函数的出现

在学习函数之前,我们同学们所有的代码都是书写在“main”函数的函数体当中。这个“main”函数被我们称之为“主函数”,我们知道整个程序是从主函数的第一句代码开始,直到最后一句代码结束。比如:字符串比较相等。

1
2
3
4
5
6
7
8
public static void main(String[] args){
//语句1
//语句2
//语句3
...
...
//语句n
}

在这样一个结构当中,我们书写了每一道程序练习题目的所有功能代码。从“水仙花数”到“小球弹起落下”、从“求学生成绩平均分”到“打各种星星图形”、从“老耗子生小耗子”到“字符串比较是否相等”我们都是这样做的。我们把所有的代码都写在一个整体模块(“main”函数)当中,然后执行JVM去依次运行每一条语句。那么,当我们的程序越来越庞大、完成的功能越来越多、实现代码越来越复杂的时候,我们也还是这么操作吗?🤔

情况1: 反复出现的代码

在一个程序实现过程中,某一部分的代码功能反复出现了多次。比如:

那么,我们完全可以把这段反复出现的代码,提取到一个单独的模块中,给它取一个名称(即:函数名),然后在需要使用到的地方进行调用就行了。

很明显,提出函数之后的设计优势非常大。首先,代码量少了很多,减少了无谓的重复劳动;其次,代码也很便于维护,如果重复代码需要被修改,那么只需要在单独的模块中修改一次即可;第三,如果能够“见名知意”的给函数命名,那么对于代码的整体阅读性也是一种提高!👍

情况2: 复杂问题简单化

除了反复出现的代码值得被书写✍️成为函数以外,还有另一种情况也使得函数被大量的设计出现。那就是当我们遇到业务逻辑比较复杂的大型应用的时候❗️
在程序设计过程中,为了处理上的方便,通常是将一个较大的任务划分为若干个较小的部分,每一部分都具有一定的处理功能,并可以分别由不同的人员来编写和调试程序。这种方式即便于设计人员能够从宏观层面分析设计项目流程,又能够组织开发人员分工合作共同完成比较复杂的任务。这种模块化开发思想,最早在编程语言中的体现就是“函数”。

在上图中,我们能清楚的看到:设计人员不用去考虑每个功能模块的实现细节,他们只需要理清楚有多少功能模块,模块与模块之间有什么样的顺序流程和数据交互即可;每个功能块可以让不同的开发人员同时并行开发,然后组合在一起即可。

函数的意义



1. 函数的出现是模块化编程思想的必然结果;
2. 设计人员不用考虑实现的细节,只需要理清楚在整个程序中有哪些模块、模块与模块的关系,这样更有利于他们对于程序宏观的控制;
3. 开发人员可以只考虑自己负责的模块的实现,达到多人同时开发的效果,在效率上大大提升;
4. 同一个函数模块被定义好之后,可以反复调用,从而增加代码的复用性;
5. 良好的函数命名,可以大大提升代码的整体可读性。

函数的语法

在了解了函数到底是什么,以及为什么要使用函数以后,我们接下来再来讨论一下Java语言中函数的具体语法。
从上面的示例中可以看到,函数是分为两个部分的:“函数的定义”和“函数的调用”。



1. 仅有函数的定义,没有函数的调用,那么程序运行起来是不会执行到函数当中的代码块;
2. 仅有函数的调用,没有函数的定义,那么程序是找不到这个函数从而无法执行只能报错。

函数的定义

由于我们只学到了基础阶段,目前我们能够看到的函数定义语法如下:

1
2
3
public static 返回类型 函数名(形参列表){
//功能的实现语句
}

虽然这个定义语法还不是Java函数语法的全部内容,不过已经包含了最最主要的部分,搞清楚它们对我们后面的学习是至关重要的。方法定义的语法由“声明”和“实现”两块组成。

⚠️Java代码的书写习惯是起始“ { ”不换行,这里是为了画图方便

声明部分

  • public static ———— 统称“修饰符”,它们是用来修饰这个函数的被调用范围和调用方式;这里关系到面向对象的知识点,我们在现阶段不讨论,大家现在请先无脑写死;
  • 返回类型 ———— 用来定义函数执行结束以后,是否有结果返回给调用者;如果没有请写void,如果有请书写返回的类型;
    ⚠️一个函数只能书写一种返回类型,在函数定义的时候只能确定其类型,不能确定其值。因为具体的返回值只有在该函数被执行的时候(调用时)才能确定。

  • 函数名 ———— 用来描述函数功能的标识符,也是用来绑定调用方到底调用哪个函数的关键;

  • 形参列表 ———— 如果函数的执行过程中,需要调用方传递一些先决数据,那么就需要定义形参列表;关键在于参数的类型、参数的个数和参数的顺序;


    ⚠️1. 如果函数不需要调用方传参,可以定义无参函数,但是“( )”不能缺少;
    ⚠️2. 由于在定义函数时,同样只能确定是否接参数,接几个参数,每个参数是什么类型的,第一个参是什么,第二个参是什么,所以在语法上此时只有参数的形式没有参数的值(其语法特性就像在定义变量一样,以便于运行起来后装入调用方传入的实际的参数值)。所以,这里的参数列表被称之为“形参列表”(参数的形式)。比如:
    1
    public static void welcome()
    1
    public static boolean login(String name, String password)
    1
    public static int addAll(int[] array)

实现部分

每个函数的实现部分代码都是依赖于该函数在设计的时候要完成什么样的功能而决定的。都是把形参作为已知条件(如果方法声明确定了形参变量),通过具体的算法和流程,完成这个函数的全部功能,并最终返回结果值(如果方法声明确定了返回类型的话)。

1
2
3
4
//完成一个打印欢迎的功能
public static void welcome() {
System.out.println("欢迎使用ATM程序👏👏👏");
}

1
2
3
4
5
6
7
//完成一个判断用户名是否是“admin”和密码是否是“123456”的登陆功能
public static boolean login(String name, String password) {
if("admin".equals(name) && "123456".equals(password)){
return true;
}
return false;
}

1
2
3
4
5
6
7
8
//完成一个数组所有元素求和的功能
public static int addAll(int[] array) {
int cnt = 0;
for(int i = 0; i < array.length; i++){
cnt += array[i];
}
return cnt;
}

函数实现部分的语法中,要注意的要点如下:

  • “{ }” ———— 函数的实现是从这对大括号的开始而开始,结束而结束的;换句话说只要书写了这对大括号,就叫做函数已经被实现了,就算括号内部一句代码都没有;

  • return ———— 重要的事情说三遍:“return不是用来返回值的!return不是用来返回值的!return不是用来返回值的!”;return是用来结束函数执行的,把执行流程返回到函数调用处。如果函数声明中有返回类型,那么函数的每种结束情况都需要在return后面跟上该类型的返回值;如果函数声明的返回类型是void,那么直接书写renturn即可结束函数;

函数的调用

当我们定义好一个函数以后,接下来就是调用了。这样,这个函数才能在执行到调用代码的时候被执行到。其基本结构如下:

1
类名.函数名(实参列表);

调用语法

由于我们目前学习的函数的修饰符固定为“public static”,所以我们都可以使用上面这个语法直接对某个函数进行访问。区别只在于:函数的定义位置和调用位置是否是在同一个类当中。如果是的话,前面的”类名.”是可以省略的。

1
2
3
4
5
6
7
8
9
10
11
public class Test{
//定义函数method
public static void method(){
System.out.println("method函数的实现");
}
public static void main(String[] args){
Test.method();//调用Test类的method函数
method();//调用本类的method函数
}
}

参数传递

参数传递是很多同学在初学函数的时候最容易搞错的地方,所以接下来我们详细讨论一下这种情况。
首先,我们分清楚形参与实参两个概念;

形参 实参
位置 方法定义处 方法调用处
语法 声明变量的语法 使用常量或变量名
含义 声明一个新的变量,用来接收函数调用处传递过来的数据值。 调用方真正需要传递到函数中的数据值; 可以是一个常量值,也是可以是调用方某个同类型变量当中的值。

其次,我们再记住一个重要的理论:Java中参数的传递只有一种形式叫“值传递”。即把实参的值传递给形参。

接下来,我们看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{
public static void welcome(int num){//num是形参
for(int i = 0; i < num; i++){
System.out.println("欢迎,欢迎,热烈欢迎!");
}
}
public static void main(String[] args){
int times = 5;
welcome(times);//times是实参
}
}

执行步骤:

  1. 程序从main方法开始,执行“int times = 5;”。内存中在main函数的作用域产生一个times变量,放入数据5;
  2. 执行调用语句”welcome(times);”,程序流程转到welcome函数;
  3. 进入welcome函数,先执行形参”int num”。内存中在welcome函数的作用域产生一个num变量;
  4. 把实参times的值,传递给形参num,完成参数传递;
  5. 开始执行welcome内部的代码。

返回值

当函数的声明中带有某种返回类型(非void)的时候,这表明这个函数执行结束以后会有一个该类型的返回值传递回调用处。如果说参数是为了把函数所需要的外部条件从调用处传递到函数内部,那么返回值则是把函数执行后的结果从函数内部传递回调用处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Scanner;
public class Test{
//判断是否是奇数
public boolean isOdd(int num){
boolean result = false;
if(num % 2 == 1){
result = true;
}
return result;
}
public static void main(String[] args){
System.out.println("请输入任意一个整数:");
Scanner scan = new Scanner(System.in);
int input = scan.nextInt();
boolean flag = isOdd(input);//isOdd函数调用处
System.out.println(flag);
}
}

执行步骤:

  1. 程序从main方法开始,当执行到“iint input = scan.nextInt();”。内存中在main函数的作用域产生一个input变量,放入用户输入的数据,比如5;
  2. “boolean flag = isOdd(input);”这条语句会先执行赋值符号的左边,即在main方法的作用域中先产生一个boolean类型的变量叫做flag;然后再调用isOdd函数;
  3. 调用isOdd函数则程序流程进入isOdd函数内部,先执行”int num”产生形参变量,把实参的值传递给形参,完成参数传递;
  4. 执行isOdd内部的代码,直到结束。由于结束语句是“return result;”,所以会在程序流程返回到方法调用处的同时,把isOdd内部变量result的值作为结果也返回给调用处;
  5. 然后通过“=”赋值符号,把这个值赋给flag变量;然后继续演main函数向下执行。

⚠️只要isOdd函数执行结束,流程就会回到调用处,同时返回值也会返到调用处,不管我们有没有用一个变量接收它。相当于,返回值就是这个函数调用表达式的结果。我们可以把这个结果用一个变量(如:flag)存起来,也可以直接用来做打印、做判断,或者其它操作。

1
2
3
4
5
6
public static void main(String[] args){
System.out.println("请输入任意一个整数:");
Scanner scan = new Scanner(System.in);
int input = scan.nextInt();
System.out.println(isOdd(input));//直接打印返回值
}
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args){
System.out.println("请输入任意一个整数:");
Scanner scan = new Scanner(System.in);
int input = scan.nextInt();
if(isOdd(input)){//利用boolean类型返回值做判断
System.out.println(input + "是奇数");
}else{
System.out.println(input + "是偶数");
}
}
当函数设计有返回值的情况下,可以直接把调用函数的表达式当成一个数据值使用!

函数调用栈

当一个函数定义好了以后,任何一个函数都可以调用它。所以很有可能会出现函数调用函数,再调用函数的情况。在这种情况下就会呈现出一种“栈”结构,也就是我们说的先进后出,即先被调用的函数会后结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test{
public static void main(String[] args){
System.out.println("main方法开始");
methodA();
System.out.println("main方法结束");
}
public static void methodA(){
System.out.println("A方法开始");
methodB();
System.out.println("A方法结束");
}
public static void methodB(){
System.out.println("B方法开始");
System.out.println("B方法结束");
}
}

执行步骤:

打印结果:

这种函数嵌套调用的执行顺序,就被称之为“函数调用栈”。

函数的设计

设计是一个非常灵活和抽象的活儿,没有对错之分,只有好与不好之别。
之所以说没有对错,是因为无论如何设计函数最基本的要求都必须是满足功能性;但满足功能要求的设计可能有很多种。
而设计的好与不好是体现在复用度的宽与窄、分离度的高与低、以后随着业务的变化是否具有更好的成长性这些方面(说白了就是随着业务变化,是否只需要很少的改动甚至是不需要改动已有的设计)。
可是要把握住这些,需要同学们不仅仅掌握语法,还必须有大量的实践与参考优秀设计的经验,这都不是一朝一夕就能够做到的。只有依靠实践和时间去积累,才会有效果。
所以,在这里只是罗列出一些简单的,同学们目前能够体会到的设计函数的原则:

1、一个函数只做一件事

完美的程序设计,每个函数应该而且只需做一件事。
比如说:把大象放进冰箱分三步:把门打开、把大象放进去、把门关上。
这样就应该写三个函数而不是一个函数拿所有的事全做了。这样结构清晰,层次分明,也好理解!

2、函数的实现规模要小

一个函数的实现语句最好不要超过100行。虽然这不是一个绝对的要求,但却不失为一个好的关注点,因为这提醒我们这个函数的功能是否还可以进行进一步的拆分,从而具有更好的分离度。

3、函数名要见名知意

这样才能方便调用者能够正确和快速的选择要使用的函数,同时也让代码具有更高的可读性。

4、参数不要过多,参数名也要见名知意

参数过多会导致在使用时容易将参数类型或顺序搞错,从而带来不必要的复杂度。

5、好的设计中,参数的有效性一定要验证

参数是从外部传入到函数内部的,其数据值是多少不由函数负责而是调用者的职责。如果调用者传入了不规范甚至错误的数据,可能会导致函数执行的失败。所以,对参数有效性进行验证是一种好的习惯。