java基础(03) - 函数

函数概念

计算机程序使用函数这个概念来解决这个问题,即使用函数来减少重复代码和分解复杂操作,本节我们就来谈谈Java中的函数,包括函数的基础和一些细节

定义函数

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

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

函数调用

Java中,任何函数都需要放在一个类中,类我们还没有介绍,我们暂时可以把类看做函数的一个容器,即函数放在类中,类中包括多个函数,Java中函数一般叫做方法,我们不特别区分函数和方法,可能会交替使用。一个类里面可以定义多个函数,类里面可以定义一个叫做main的函数,形式如:

1
2
3
4
5
public static void main(String[] args) {
...
// 函数a a();
// 函数b b();
}

函数传参

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

值传递和引用传递

  • 基本数据类型传值,对形参的修改不会影响实参
  • 引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象
  • String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。

函数命名以及重载

每个函数都有一个名字,这个名字表示这个函数的意义,名字可以重复吗?在不同的类里,答案是肯定的,在同一个类里,要看情况。

同一个类里,函数可以重名,但是参数不能一样,一样是指参数个数相同,每个位置的参数类型也一样,但参数的名字不算,返回值类型也不算。换句话说,函数的唯一性标示是:类名_函数名_参数1类型_参数2类型_…参数n类型。

同一个类中函数名字相同但参数不同的现象,一般称为函数重载。为什么需要函数重载呢?一般是因为函数想表达的意义是一样的,但参数个数或类型不一样。比如说,求两个数的最大值,在Java的Math库中就定义了四个函数,如下所示:

image-20211220154756946

函数是计算机程序的一种重要结构,通过函数来减少重复代码,分解复杂操作是计算机程序的一种重要思维方式。本节我们介绍了函数的基础概念,还有关于参数传递、返回值、重载、递归方面的一些细节。

函数调用的基本原理

我们之前谈过程序执行的基本原理:CPU有一个指令指示器,指向下一条要执行的指令,要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。

基本上,这依然是成立的,程序从main函数开始顺序执行,函数调用可以看做是一个无条件跳转,跳转到对应函数的指令处开始执行,碰到return语句或者函数结尾的时候,再执行一次无条件跳转,跳转回调用方,执行调用函数后的下一条指令。

但这里面有几个问题:

  • 参数如何传递?
  • 函数如何知道返回到什么地方?在if/else, for中,跳转的地址都是确定的,但函数自己并不知道会被谁调用,而且可能会被很多地方调用,它并不能提前知道执行结束后返回哪里。
  • 函数结果如何传给调用方?

解决思路是使用内存来存放这些数据,函数调用方和函数自己就如何存放和使用这些数据达成一个一致的协议或约定。这个约定在各种计算机系统中都是类似的,存放这些数据的内存有一个相同的名字,叫

栈是一块内存,但它的使用有特别的约定,一般是先进后出,类似于一个桶,往栈里放数据,我们称为入栈,最下面的我们称为栈底,最上面的我们称为栈顶,从栈顶拿出数据,通常称为出栈。栈一般是从高位地址向低位地址扩展,换句话说,栈底的内存地址是最高的,栈顶的是最小的。

计算机系统主要使用栈来存放函数调用过程中需要的数据,包括参数、返回地址,函数内定义的局部变量也放在栈中。计算机系统就如何在栈中存放这些数据,调用者和函数如何协作做了约定。返回值不太一样,它可能放在栈中,但它使用的栈和局部变量不完全一样,有的系统使用CPU内的一个存储器存储返回值,我们可以简单认为存在一个专门的返回值存储器。 main函数的相关数据放在栈的最下面,每调用一次函数,都会将相关函数的数据入栈,调用结束会出栈。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Sum {

public static int sum(int a, int b) {
int c = a + b;
return c;
}

public static void main(String[] args) {
int d = Sum.sum(1, 2);
System.out.println(d);
}

}

存储逻辑

image-20211220165332885

start
main方法开始执行 >
main参数:arg 和 局部变量:d 入栈 >
调用进入sum方法,方法参数 a 和 b 入栈 >
记录main方法当前执行代码行数指令 入栈 >
sum方法返回结果值3存储寄存器,并记录结果值 入栈 >
sum方法执行完毕,方法内部局部变量 a b 销毁,返回结果销毁 >
end

变量的生命周期

定义一个变量就会分配一块内存,但我们并没有具体谈什么时候分配内存,具体分配在哪里,什么时候释放内存。

从以上关于栈的描述我们可以看出,函数中的参数和函数内定义的变量,都分配在栈中,这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了。但这个说法主要针对基本数据类型除了局部变量存放在栈中,其它变量都在堆中, 接下来我们谈数组和对象。

数组和对象

对于数组和对象类型,我们介绍过,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址,实际的内容空间一般不是分配在栈上的,而是分配在堆(也是内存的一部分,后续文章介绍)中,但存放地址的空间是分配在上的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ArrayMax {

public static int max(int min, int[] arr) {
int max = min;
for(int a : arr){
if(a>max){
max = a;
}
}
return max;
}

public static void main(String[] args) {
int[] arr = new int[]{2,3,4};
int ret = max(0, arr);
System.out.println(ret);
}

}

image-20211220165347746

函数调用的成本

从函数调用的过程我们可以看出,调用是有成本的,每一次调用都需要分配额外的栈空间用于存储参数、局部变量以及返回地址,需要进行额外的入栈和出栈操作

在递归调用的情况下,如果递归的次数比较多,这个成本是比较可观的,所以,如果程序可以比较容易的改为别的方式,应该考虑别的方式。

函数调用主要是通过栈来存储相关数据的,系统就函数调用者和函数如何使用栈做了约定,返回值我们简化认为是通过一个专门的返回值存储器存储的。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!