一、方法调用机制
1、方法调用机制详细介绍
下面对方法调用在内存中的情况进行分析,以下面的代码为例:
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.name = "张三";
person.age = 18;
int res = person.calcSum(1,2);
}
}
class Person {
String name;
int age;
//求两个数字的和的方法
public int calcSum(int num1,int num2) {
return (num1 + num2);
}
}
在执行语句
Person person = new Person();
时,会在堆区创建一个对象,然后在 main 函数的栈帧中会创建一个对象引用变量,这个引用变量中存储的引用就是刚才创建的对象的引用。
然后执行下面这两句代码,
person.name = "张三";
person.age = 18;
对对象的属性初始化。
然后执行下面的代码,
int res = person.calcSum(1,2);
这里在调用对象 person 的方法时,会再开辟一个栈帧,就是 calSum 的栈帧,
方法内的局部变量和方法的参数在被调用时存储在方法的栈帧中。
这里的参数是基本数据类型,所以传参进行的是值拷贝,将实际参数的值直接拷贝给在 calSum 栈帧中的形式参数变量,然后进行计算,最后返回值,在方法返回后,它的栈帧就会被销毁,将返回值赋给变量 res。
这里我们发现在调用一个方法时会开辟方法对应的栈帧,再这个方法返回时,它的栈帧就会被销毁。
最后当程序退出时,main 函数栈帧也会被销毁。
二、方法调用细节
方法内部不能再定义方法。
1、访问修饰符
类的属性有访问修饰符,类的成员方法也有访问修饰符,与类的属性一致。
访问修饰符:
在Java中,访问修饰符(Access Modifiers)用于控制类、方法和属性(字段)的可见性。它们决定了其他类是否能够访问特定的类成员(属性或方法)。Java中有四种主要的访问修饰符:
public
protected
- (什么都不写时的访问级别为默认)
private
修饰符 当前类 同一个包 子类(不同包) 其他包 public
√ √ √ √ protected
√ √ √ × 默认
√ √ × × private
√ × × ×
2、方法返回值
返回数据类型
如果方法声明中有返回数据类型,则方法体中的执行语句必须有 return
语句。return
语句用来返回与返回值类型一致或兼容(可以自动类型转换)的值。
无返回值类型
如果方法返回值类型是 void
,则方法体中可以有 return
语句,就像下面这样:
return;
只写一个 return
,不返回任何值。或者可以没有 return
语句。void
表示方法不返回任何值。
3、每个方法只能返回一个值
每个方法只能返回一个值,那如果要返回多个值怎么办呢,就可以使用以下方法:
public int[] example(int num1,int num2) {
//...
int[] arr = new int[2];
//...
return arr;
}
这样在堆区创建一个数组对象,然后返回其引用,就可以实现返回的引用可以访问一个数组,就可以通过这个数组存储多个值了。
对于这里所说的返回数组类型,实际上返回的不是一整个数组,而是数组的引用。
所以在我们调用这个方法时,也要使用数组引用变量来接受其返回值。
int[] arr = example(1,2);
4、形参和实参
形参是指在方法声明中定义的参数,它们作为占位符,用于接收调用者传递的实际参数值。
- 定义位置:形参定义在方法的参数列表中。
- 作用范围:形参的作用范围仅限于方法内部。
- 数据类型:形参需要指定类型,可以是基本数据类型或引用数据类型。
public class Example {
// 形参是 a 和 b
public int add(int a, int b) {
return a + b;
}
}
在上述示例中,a
和 b
是形参。
实参是指在方法调用时传递给方法的实际值或对象,这些实际值或对象被传递给形参。
- 定义位置:实参在方法调用时提供。
- 传递方式:实参的值或引用被复制给形参,在方法执行过程中使用。
- 类型匹配:实参的类型必须与形参的类型兼容。
public class Test {
public static void main(String[] args) {
Example example = new Example();
// 实参是 5 和 3
int result = example.add(5, 3);
System.out.println("Result: " + result);
}
}
在上述示例中,5
和 3
是实参。
在方法调用时,实参的类型必须与形参的类型匹配或兼容。例如,如果形参是 int
类型的,实参也必须是 int
类型或可以隐式转换为 int
类型。
三、方法传参机制
上面我们提到方的形参和实参,下面我们介绍一下方法的传参机制。Java中的方法参数传递机制是值传递(Pass-by-Value),对于基本类型的参数,传递的是数据的副本;对于引用类型的参数,传递的是对象的引用副本。
1、基本数据类型传参
下面我们看一个经典的例子:
public class Test {
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
public static void main(String[] args) {
int num1 = 1;
int num2 = 2;
swap(num1,num2);
System.out.println("num1 = " + num1 + "\nnum2 = " + num2);
}
}
运行这段代码,运行结果为:
可以发现这里两个变量的值并未完成交换。这是为什么呢?
在主函数中调用 swap 函数时,会开辟 swap 函数的栈帧,然后形参变量会存储在 swap 函数的栈帧中,然后会将主函数中的两个变量的值拷贝(赋值)给 swap 函数栈帧中的两个形参变量。
然后会进行 swap 函数栈帧中的两个形参变量的值的交换,这个操作不会影响 main 函数栈帧中的两个实参变量,因为 swap 栈帧中的两个形参变量和 main 函数栈帧中的两个实参变量是相互独立的变量。
然后 swap 函数的语句执行完成后,swap 栈帧会被销毁,两个 swap 中的形参变量也被销毁。
这时 main 函数中的两个实参变量没有受到任何影响,所以还是原样,所以实参的值没有被交换。
所以说对于基本数据类型(如 int
、float
、char
等),传递的是值的副本。方法内部对形参的修改不会影响实际参数的值。因为形参和实参是两个独立的变量,修改其中一个变量的值对另一个变量没有影响。
例如:
public class ValuePassing {
public static void modifyPrimitive(int number) {
number = 10;
}
public static void main(String[] args) {
int original = 5;
modifyPrimitive(original);
System.out.println("Original value after method call: " + original);
}
}
输出结果:
Original value after method call: 5
在这个示例中,original
的值被复制给 number
,方法内部对 number
的修改不会影响 original
的值。original
和 number
是两个相互独立的变量,original
在 main 函数栈帧中,number
在 modifyPrimitive 函数栈帧中。修改其中一个变量的值不影响另一个变量。
2、引用类型传参
下面我们看一个例子:
public class Test {
public static void modify(Person person) {
person.age = 0;
}
public static void main(String[] args) {
Person student = new Person();
student.age = 19;
modify(student);
System.out.println(student.age);
}
}
class Person {
String name;
int age;
}
我们运行这段代码,运行结果为:
这里为什么可以在方法内对数据进行改动呢?下面我们详细分析:
首先在主函数中创建了一个 Person 类的对象引用变量 student,然后再堆区中创建对象,将对象的引用返回给对象引用变量。
然后将 student 的 age 属性更改为了 19。
然后就调用了 modify 函数,这时 modify 的形参 person 就会存储在 modify 函数栈帧中,
然后实参 student 就会将其中村村的引用的副本赋值给形参 person,也就是将图中的 0xffff0011 赋值给形参,然后形参中存储的引用就也是 0xffff0011 了,这时使用形参也能访问到 student 这个对象了。
然后 modify 函数中的 person.age = 0; 语句就会访问 student 对象,然后修改其 age 属性。这时 student.age 就被修改成 0 了。
这里在方法内部使用对象引用变量 person 存储的引用访问对应的对象,然后改动对象的属性,实参引用的对象的属性也发生了变化,是因为这里的形参中的引用与实参中的引用是指向同一个对象的引用。
如果形参中的引用改变了的话,就没办法改动实参引用的对象的属性了。详细看下面的这个例子。
下面再看另一个例子:
public class Test {
public static void reassign(Person person) {
person = new Person();
person.age = 18;
}
public static void main(String[] args) {
Person student = new Person();
student.age = 20;
reassign(student);
System.out.println(student.age);
}
}
class Person {
String name;
int age;
}
运行结果:
下面我们进行详细分析:
首先在主函数中创建了一个 Person 类的对象引用变量,然后在堆区中创建对象 student,将对象的引用返回给对象引用变量。
Person student = new Person();
student.age = 20;
然后将 student 对象的 age 属性改为 19。
然后调用 reassign 函数,会开辟相应的函数栈帧,这时形参会被实参的拷贝赋值,实参是对象引用变量 student,实参中存储的是一个对象的引用,然后将这个引用赋值给形参。这时形参也指向 student 对象,形参也可以访问 student 对象。
然后 reassign 函数又在堆区中创建类一个 Person 类对象,
person = new Person();
person.age = 18;
将对象的引用返回给形参 person,这时 person 中存储的就是这个新创建的对象的引用了,person 就不能访问 student 对象了。
然后将这个新创建的对象的 age 属性改为18。
然后 reassign 函数语句执行完毕,reassign 函数栈帧就会被销毁,然后那个新创建的对象也因为失去被引用的机会而被垃圾回收机制回收。
这时我们发现 student 对象的 age 属性并没有什么改变。这是因为在 reassign 函数内对形参重新分配一个对象的引用对实参是没有影响的,因为形参和实参是两个相互独立的对象引用变量。
由此我们知道,对于引用数据类型(如对象和数组),传递的是对象的引用的副本。如果形参和实参存储的引用是同一个的话,方法内部对形参引用的对象进行修改会影响实际参数引用的对象,但对形参重新分配引用不会影响实际参数存储的引用。因为形参和实参是两个独立的对象引用变量。
四、方法调用实例
在了解到了上面这么多方法的使用注意事项后,我们可以实现一些实例,加增加方法使用的熟练度。
1、克隆对象
public class Test {
public static Person copyPerson(Person person) {
Person newPerson = new Person();//在堆区创建一个新的对象
newPerson.name = person.name;//将原来的对象的属性赋值给新的对象
newPerson.age = person.age;
return newPerson;//返回新对象的引用
}
public static void main(String[] args) {
Person p = new Person();
p.name = "张三";
p.age = 19;
Person p1 = copyPerson(p);//调用对象拷贝方法
System.out.println(p1.name + " " + p1.age);
System.out.println("两个对象是否是同一个 " + (p == p1));//通过比较引用确定两个对象是否相等
}
}
class Person {
String name;
int age;
}
运行结果: