https://weread.qq.com/web/reader/b51320f05e159eb51b29226

编程基础

1.6. 函数的用法

如果需要经常做某一种操作,则类似的代码需要重复写很多遍。
比如在一个数组中查找某个数,第一次查找一个数,第二次可能查找另一个数,每查一个数,类似的代码都需要重写一遍,很罗唆。
另外,有一些复杂的操作,可能分为很多个步骤,如果都放在一起,则代码难以理解和维护。
计算机程序使用函数这个概念来解决这个问题,即使用函数来减少重复代码和分解复杂操作

Java中的函数一般叫做方法

函数的主要组成部分有以下几种。
1)函数名字:名字是不可或缺的,表示函数的功能。
2)参数:参数有0个到多个,每个参数由参数的数据类型和参数名字组成。
3)操作:函数的具体操作代码。
4)返回值:函数可以没有返回值,如果没有返回值则类型写成void,如果有则在函数代码中必须使用return语句返回一个值,这个值的类型需要和声明的返回值类型一致。
5)修饰符:Java中函数有很多修饰符,分别表示不同的目的,本节假定修饰符为public static,且暂不讨论这些修饰符的目的。

程序执行基本上只有顺序执行、条件执行和循环执行,但更完整的描述应该包括函数的调用过程。

定义函数就是定义了一段有着明确功能的子程序,但定义函数本身不会执行任何代码,函数要被执行,需要被调用

1.6.0 函数调用

调用函数需要传递参数并处理返回值。

  • 区别形参和变量

关于参数传递,简单总结一下,定义函数时声明参数,实际上就是定义变量,只是这些变量的值是未知的,调用函数时传递参数,实际上就是给函数中的变量赋值。

1.6.1 参数传递

有两类特殊类型的参数:数组和可变长度的参数。
可变长度参数实际上会转换为数组参数,也就是说,函数声明max(int min, int…a)实际上会转换为max(int min, int[] a),在main函数调用max(0,2,4,5)的时候,实际上会转换为调用max(0, new int[]{2,4,5}),使用可变长度参数主要是简化了代码书写。

1.6.2 理解返回

强调下return的含义。函数返回值类型为void时,return不是必需的,在没有return的情况下,会执行到函数结尾自动返回。return用于显式结束函数执行,返回调用方。
函数返回值类型为void也可以使用return,即“return; ”,不用带值,含义是返回调用方,只是没有返回值而已。
return可以用于函数内的任意地方,可以在函数结尾,也可以在中间,可以在if语句内,可以在for循环内,用于提前结束函数执行,返回调用方。
函数的返回值最多只能有一个,那如果实际情况需要多个返回值呢?比如,计算一个整数数组中的最大的前三个数,需要返回三个结果。这个可以用数组作为返回值,在函数内创建一个包含三个元素的数组,然后将前三个结果赋给对应的数组元素。
如果实际情况需要的返回值是一种复合结果呢?比如,查找一个字符数组中所有重复出现的字符以及重复出现的次数。这个可以用对象作为返回值,我们在第3章介绍类和对象。虽然返回值最多只能有一个,但其实一个也够了。

1.6.3 重复的命名

每个函数都有一个名字,这个名字表示这个函数的意义。
名字可以重复吗?

  • 在不同的类里,答案是肯定的。在同一个类里,要看情况。
  • 同一个类里,函数可以重名,但是参数不能完全一样,即要么参数个数不同,要么参数个数相同但至少有一个参数类型不一样。

同一个类中函数名相同但参数不同的现象,一般称为函数重载。为什么需要函数重载呢?一般是因为函数想表达的意义是一样的,但参数个数或类型不一样。

1.6.4 调用的匹配过程

之前介绍函数调用的时候,我们没有特别说明参数的类型。这里说明一下,参数传递实际上是给参数赋值,调用者传递的数据需要与函数声明的参数类型是匹配的,但不要求完全一样。什么意思呢?Java编译器会自动进行类型转换,并寻找最匹配的函数,比如:
image.png
参数是字符类型的,但Math并没有定义针对字符类型的max函数,这是因为char其实是一个整数(我们在2.4节会说明), Java会自动将char转换为int,然后调用Math. max(int a, int b),屏幕会输出整数结果98。
如果Math中没有定义针对int类型的max函数呢?调用也会成功,会调用long类型的max函数。如果long也没有呢?会调用float型的max函数。如果float也没有,会调用double型的。Java编译器会自动寻找最匹配的

总结:在只有一个函数的情况下,即没有重载,只要可以进行类型转换,就会调用该函数,在有函数重载的情况下,会调用最匹配的函数。

1.6.5 递归函数

函数大部分情况下都是被别的函数调用的,但其实函数也可以调用它自己,调用自己的函数就叫递归函数
递归函数形式上往往比较简单,但递归其实是有开销的,而且使用不当,可能会出现意外的结果,
image.png
image.png
系统并不会给出任何结果,而会抛出异常。异常类型为java.lang.StackOverflowError,这是什么意思呢?这表示栈溢出错误,要理解这个错误,我们需要理解函数调用的实现原理,我们1.7节介绍。

那递归不可行的情况下怎么办呢?递归函数经常可以转换为非递归的形式,通过循环实现。
image.png

1.7. 函数调用的基本原理

java.lang.StackOverflowError,理解这个错误,需要理解函数调用的实现机制。

1.7.1 栈的概念

程序执行的基本原理:CPU有一个指令指示器,指向下一条要执行的指令,要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。
函数调用的基本原理:可以看作一个无条件跳转,跳转到对应函数的指令处开始执行,碰到return语句或者函数结尾的时候,再执行一次无条件跳转,跳转回调用方,执行调用函数后的下一条指令。
1)参数如何传递?
2)函数如何知道返回到什么地方?在if/else、for中,跳转的地址都是确定的,但函数自己并不知道会被谁调用,而且可能会被很多地方调用,它并不能提前知道执行结束后返回哪里。
3)函数结果如何传给调用方?

解决思路是使用内存来存放这些数据,存放这些数据的内存有一个相同的名字,叫
栈一般是从高位地址向低位地址扩展,换句话说,栈底的内存地址是最高的,栈顶的是最低的。(字节理解)
计算机系统主要使用栈来存放函数调用过程中需要的数据,包括参数、返回地址,以及函数内定义的局部变量。
计算机系统就如何在栈中存放这些数据,调用者和函数如何协作做了约定。
返回值不太一样,它可能放在栈中,但它使用的栈和局部变量不完全一样,有的系统使用CPU内的一个存储器存储返回值,我们可以简单认为存在一个专门的返回值存储器。
main函数的相关数据放在栈的最下面,每调用一次函数,都会将相关函数的数据入栈,调用结束会出栈。
->StackOverflowError

1.7.2 函数执行的基本原理

示例

image.png
当程序在main函数调用Sum.sum之前
image.png
在程序执行到Sum.sum的函数内部,即第5行,准备返回之前
image.png
【值传递】
在main函数调用Sum.sum时,首先将参数1和2入栈,然后将返回地址(也就是调用函数结束后要执行的指令地址)入栈,
接着跳转到sum函数,在sum函数内部,需要为局部变量c分配一个空间,而参数变量a和b则直接对应于入栈的数据1和2,在返回之前,返回值保存到了专门的返回值存储器中。
在调用return后,程序会跳转到栈中保存的返回地址,即main的下一条指令地址,而sum函数相关的数据会出栈
main的下一条指令是根据函数返回值给变量d赋值,返回值从专门的返回值存储器中获得。

1.7.3 数组和对象的内存分配

定义一个变量就会分配一块内存,但我们并没有具体谈什么时候分配内存,具体分配在哪里,什么时候释放内存。
从以上关于栈的描述我们可以看出,函数中的参数和函数内定义的变量,都分配在栈中,这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了。但这个说法主要针对基本数据类型,接下来我们介绍数组和对象。
对于数组和对象类型,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址,实际的内容空间一般不是分配在栈上的,而是分配在堆中,但存放地址的空间是分配在栈上的。
image.png
在程序执行到max函数的return语句之前
image.png
对于数组arr,在栈中存放的是实际内容的地址0x1000,存放地址的栈空间会随着入栈分配,出栈释放,但存放实际内容的堆空间不受影响(但说堆空间完全不受影响是不正确的)。在这个例子中,当main函数执行结束,栈空间没有变量指向它的时候,Java系统会自动进行垃圾回收,从而释放这块空间。

1.7.4 递归调用的原理

通过栈的角度来理解一下递归函数的调用过程
image.png
在执行factorial(3)之前
image.png
注意,返回值存储器是没有值的
在执行factorial(2)之前
image.png
栈的深度增加了,返回值存储器依然为空,就这样,每递归调用一次,栈的深度就增加一层,每次调用都会分配对应的参数和局部变量,也都会保存调用的返回地址
在执行factorial(2),return之前
image.png
这个时候,终于有返回值了,我们将factorial简写为f。f(0)的返回值为1; f(0)返回到f(1), f(1)执行1f(0),结果也是1;然后返回到f(2), f(2)执行2f(1),结果是2;接着返回到f(3), f(3)执行3f(2),结果是6;然后返回到f(4),执行4f(3),结果是24。

1.7.5 小结

函数调用主要是通过栈来存储相关的数据,系统就函数调用者和函数如何使用栈做了约定返回值可以简单认为是通过一个专门的返回值存储器存储的
从函数调用的过程可以看出,调用是有成本的每一次调用都需要分配额外的栈空间用于存储参数、局部变量以及返回地址需要进行额外的入栈和出栈操作
在递归调用的情况下,如果递归的次数比较多,这个成本是比较可观的,所以,如果程序可以比较容易地改为其他方式,应该考虑其他方式。
栈的空间不是无限的,一般正常调用都是没有问题的,但如果栈空间过深,系统就会抛出错误java.lang.StackOverflowError,即栈溢出。