Java的值传递
前因
前几天在力扣刷题,就是经典的杨辉三角 这道题,这道题虽然是简单题,但是在写的时候还是遇见了很多细节上的问题。
这是我的完整代码:
package cuit.epoch.pymjl.practice;
import java.util.ArrayList;
import java.util.List;
/**
* @author Pymjl
* @version 1.0
* @date 2022/5/5 17:05
**/
public class Main {
public static void main(String[] args) {
System.out.println(generate(5));
}
public static List<List<Integer>> generate(int numRows) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> row = new ArrayList<>();
for (int i = 0; i < numRows; i++) {
row.add(0, 1);
for (int j = 1; j < row.size() - 1; j++) {
row.set(j, row.get(j) + row.get(j + 1));
}
res.add(row);
}
return res;
}
}
这是输出结果:
这是预计结果:
欸,这不对呀,为什么我的输出结果全是最后一次循环的结果呢。然后仔细一想,因为每次循环的向集合中添加的都是一个子集和,会不会是这里的问题。然后经过Debug,最终定位到问题代码:
for (int i = 0; i < numRows; i++) {
row.add(0, 1);
for (int j = 1; j < row.size() - 1; j++) {
row.set(j, row.get(j) + row.get(j + 1));
}
//问题代码
res.add(row);
}
这里的逻辑是每次将修改后的集合row添加到结果集res中,问题就在这,我是直接将修改后的row添加到res里面,然而集合row是一个引用对象,res里面保存的是row的地址,所以后面我们对row的修改同样也会同步到之前添加到res中的记录中。意识到这个问题之后,我对代码做出修改,每次传递的都是同一个List集合的地址,我们为了防止后续的操作影响前面的记录,我们直接new 一个对象存进去就好了
res.add(new ArrayList<>(row));
运行结果:
这下就没错了。很多学习过C语言的小伙伴肯定都会说啦,这个错误原因我知道,当实参为基本类型的时候给形参传得是值,当实参为引用类型的时候传的是地址,所以刚才我们一直传入的是row的地址,才会造成这个原因。
最开始我也是这么认为的,可是后来我发现我错的离谱。
形参&实参
我们都知道,参数在程序语言中分为:
- 实参:在调用有参函数时,主调函数和被调函数之间有数据传递关系。实参是在主调函数中传递给被调函数/方法的参数,并需要有确定的值。
- 形参:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数
举个简单的例子:
package cuit.epoch.pymjl.practice;
/**
* @author Pymjl
* @version 1.0
* @date 2022/5/5 17:29
**/
public class Test {
public static void main(String[] args) {
//msg是实参
String msg = "Hello world";
say(msg);
}
public static void say(String msg) {
//这里的msg是形参,用于接收主方法传来的实参
System.out.println(msg);
}
}
值传递与引用传递
上面提到了,当我们调用一个有参函数的时候,会把实际参数传递给形式参数。但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递。我们来看下程序语言中是如何定义和区分值传递和引用传递的。
- 值传递:方法接收的是实参值的拷贝,会创建副本。
- 引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
很多程序语言提供了上面两种传递方式,比如C就支持两种传递方式。但是在Java中只有值传递!!!
很多读者小伙伴读到这里肯定会骂:博主这不是睁着眼睛说瞎话吗,如果Java中只有值传递那么上面算法题的错误是怎么回事?大家切勿心急,听我给你们娓娓道来。
为什么Java中只有值传递?
实践是检验真理的唯一标准。我们先通过几个例子来分析Java中的实参与形参的传递,最后再来进行总结
- 案例一:基本类型
package cuit.epoch.pymjl.practice;
/**
* @author Pymjl
* @version 1.0
* @date 2022/5/5 17:29
**/
public class Test {
public static void main(String[] args) {
int num1 = 1;
int num2 = 2;
swap(num1, num2);
System.out.println("主方法中的num1=" + num1 + " num2=" + num2);
}
public static void swap(int num1, int num2) {
int temp = num1;
num1 = num2;
num2 = temp;
System.out.println("swap方法中的num1=" + num1 + " num2=" + num2);
}
}
运行结果
解析:在swap()
方法中num1
与num2
值的交换并不会影响到主方法中的num1
和num2
,因为这是值传递的方式,在实参给形参传递值的时候是拷贝的一份实参的副本传入swap()
方法中,所以无论在方法中如何操作,改变的都属副本的值,并不会对原数据造成影响。
- 案例二:引用类型
我们先定义一个学生类
package cuit.epoch.pymjl.practice;
/**
* @author Pymjl
* @version 1.0
* @date 2022/5/5 18:42
**/
public class Student {
private String name;
private Integer age;
public Student() {
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
然后编写主测试类
/**
* @author Pymjl
* @version 1.0
* @date 2022/5/5 17:05
**/
public class Main {
public static void main(String[] args) {
Student student = new Student("小明", 12);
System.out.println("调用方法前: " + student);
test(student);
System.out.println("调用方法后: " + student);
}
public static void test(Student student) {
student.setName("明明");
}
}
运行结果:
看到这儿很多读者肯定要反驳我了:你看,明明在方法中改变的值影响到了原参数,这不是显而易见的引用传递吗?
非也!非也!
我再重复一遍Java中只有值传递,至于为什么这里在方法中的操作影响到了原来的值,这是因为Student是我们自定义的对象,传入的是这个对象的引用的副本罢了,也就是说:student的地址值被拷贝了一份传入了方法,然后我们在方法中的操作是直接作用在这个地址所指向的的目标内存区域,所以主方法中指向这块区域的对象值也随之改变。
我们再写一个例子来证明它是值传递
package cuit.epoch.pymjl.practice;
/**
* @author Pymjl
* @version 1.0
* @date 2022/5/5 17:05
**/
public class Main {
public static void main(String[] args) {
Student student1 = new Student("小明", 12);
Student student2 = new Student("小红", 20);
test(student1, student2);
System.out.println("-----------main方法中-------------");
System.out.println("小明:" + student1);
System.out.println("小红:" + student2);
}
public static void test(Student student1, Student student2) {
Student temp = student1;
student1 = student2;
student2 = temp;
System.out.println("-----------test方法中-------------");
System.out.println("小明:" + student1);
System.out.println("小红:" + student2);
}
}
运行截图
解析:在test方法中的修改并没有同步到主方法中。这是因为传入test方法的只是对引用的拷贝,而在test方法中的操作仅仅是对拷贝的引用进行交换,让他们交换了指向的目标内存区域。但是在主方法中的引用并没有被改变,还是指向的原来的内存区域。
小结
Java中只有值传递!!!
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本
解析:在test方法中的修改并没有同步到主方法中。这是因为传入test方法的只是对引用的拷贝,而在test方法中的操作仅仅是对拷贝的引用进行交换,让他们交换了指向的目标内存区域。但是在主方法中的引用并没有被改变,还是指向的原来的内存区域。
小结
Java中只有值传递!!!
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本