通过这两天的实习面试,发现自己的基础实在烂的不行,先不打算投简历了,安安心心的在家学了几个月,把最基本的东西全部搞懂再说。之前看到别人也在写类似的模块,觉得挺好的,因此我打算每天(也有可能几天..)整理一个常考的知识点,帮助自己整理和巩固,加油把
一、前言
先分清楚两个概念
- 按值传递:方法接收的是调用者提供的值
- 按引用传递:方法接收的是调用者提供的变量地址
《Java 核心技术》中说:“Java程序设计语言总是采用按值来调用的,即,方法得到的是所有参数值的一个拷贝。因为传递过来的相当于是一个副本,因此方法只能改变这个副本所对应的值或者对象。
二、分析
2.1 将基本数据类型作为参数传递
我们可以直接看一个值传递的例子
public void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
@Test
void testObject() {
int a = 100;
int b = 200;
swap(100, 200);
System.out.println("a: " + a);
System.out.println("b: " + b);
}
//输出
a: 100
b: 200
可见,a 和 b 的值在经过“所谓的”交换函数之后,并没能把值交互。这是为什么呢?
这就是因为java是值传递的。也就是说,我们在调用一个需要传递参数的函数时,传递给函数的参数并不是我们传进去的参数本身,而是它的副本。
以这个例子来说,当我们调用 swap 这个函数时,并不是传入的真正的 a 和 b 这两个参数,而是他们俩的复制品,比如我们定义这两个复制品为 x 和 y,此时 x 会指向另一个 100,y 会指向另一个 200。之后在方法 swap 中的所有操作,其实都是基于这两个复制出来的变量 x 和 y 进行着的。尽管 x 和 y 的值确实交换了,但是他们值的改变并不能影响到 a 和 b。
再来看一个例子:
void foo(int val) {
val = 100;
}
void foo1(String text) {
text = "win";
}
@Test
public void print() {
int val = 200;
foo(val);
System.out.println(val);
String text = "hello";
foo1(text);
System.out.println(text);
}
无可厚非,输出分别是 200 和 hello。可以用图片来描述一下流程
首先,在调用方法之前,会先创建一个形参 val
当调用 foo 函数的时候,实参 val 将自身的值拷贝一份,将拷贝的副本传给形参
当执行函数中的语句时,其实都是对拷贝的那个参数,即形参 val 进行赋值,可以看到,我们将他直接变为了 100,而实际的参数 val,还是原来的 200
2.2 将对象引用作为参数传递
顾名思义,就是方法的参数是一个类的引用
class People {
int age;
}
@Test
void testObject() {
People p1 = new People();
People p2 = new People();
p1.age = 10;
p2.age = 15;
System.out.println("p1.age: " + p1.age + " p2.age: " + p2.age);
p1 = p2;
System.out.println("p1.age: " + p1.age + " p2.age: " + p2.age);
p1.age = 30;
System.out.println("p1.age: " + p1.age + " p2.age: " + p2.age);
}
//输出
p1.age: 10 p2.age: 15
p1.age: 15 p2.age: 15
p1.age: 30 p2.age: 30
通过图示我们可以分析一下
首先在栈中建立两个引用 p1 和 p2,分别指向堆中 new 出来的对象
执行 p1=p2 这个语句,把栈中 p1 指向 p2 在堆中指向的位置,即第二个 People 对象,此时 p1 和 p2 都指向了堆中第二个 People 对象
执行 p1.age=30 这个语句,即把第二个 People 对象的 age 属性变为30,由于 p1 和 p2 都指向这个对象,因此 p1 和 p2 的 age 属性都是 30
我们可以再来看一个例子
void foo(StringBuffer stringBuffer) {
stringBuffer.append("world");
}
@Test
void bufferTest2() {
StringBuffer sb = new StringBuffer("hello");
foo(sb);
System.out.println(sb);
}
输出结果是 helloworld,表明原来的值已被改变。照例,我们画个图
一开始,栈中的 StringBuffer 引用指向堆中的 StringBuffer 对象,该对象里面的值为 “hello”
当使用方法时,形参会先产生一个 StringBuffer 引用 stringBuffer,然后指向堆中的对象
然后调用方法,append 方法直接改变的就是原来 String 的值。可以看到,此时堆中 StringBuffer 对象中的值已经改变,此时两个引用 sb 和 stringBuffer 都指向同一个对象
我们再来看另一个例子
void foo1(StringBuffer stringBuffer) {
stringBuffer = new StringBuffer("world");
}
@Test
void bufferTest2() {
StringBuffer sb = new StringBuffer("hello");
foo1(sb);
System.out.println(sb);
}
此时输出的值没有改变,还是 “hello”。这是为什么呢?
当调用 fool 方法的时候,实际在栈中又新创建了一个 StringBuffer 的引用 stringBuffer,这个引用重新指向了一个新的 StringBuffer 对象。
所以即使当执行方法之后,输出 sb,依旧还是原来的 sb 指向的对象中的值,即 “hello”
2.3 值传递和引用传递
由于 C++ 有值传递和引用传递两种方式,那么 Java 呢?实际上,Java 是采用的值传递
class Employee {
int x;
Employee(int a) {
this.x = a;
}
}
public class ObjectTest2 {
static void swap(Employee x, Employee y){
Employee temp = x;
x = y;
y = temp;
}
public static void main(String[] args) {
Employee employee = new Employee(100);
Employee employee1 = new Employee(200);
System.out.println("交换前:" + employee.x + " " + employee1.x);
swap(employee, employee1);
System.out.println("交换后:" + employee.x + " " + employee1.x);
}
}
结果是:
交换前:100 200
交换后:100 200
这个例子很明显,如果 Java 是引用传递,那么在调用完那个方法之后,应该可以实现数据的交换,但实际上,并没有交换。
可以看到,两个对象引用传到方法之后,拷贝出了两个对象引用 x, y,x 指向 的是第一个 Employee 对象,y指向的是第二个。然后 swap() 方法中交换的其实仅仅是拷贝所指向的地址
最后,方法结束,x 和 y 也被回收,而最原始的两个对象引用 employee 和 employee1 还是指向之前的两个对象
那为什么不是引用传递呢?
其实可以联想一下 C++ 是如何进行引用传递的
void swap(int *p1, int *p2){
int *temp;
temp = p1;
p1 = p2;
p2 = temp;
}
可以看到,同样也是传入两个参数的地址,然后直接改变两个参数所在的内存地址。这个时候从内存中取出对应的数值应该就是相反的值了
然后我们再看 Java,同样传入的是地址,那么他是如何实现的呢?传入一个引用,拷贝一份,再传入一个,再拷贝一个,之后的事情就很简单了,直接对拷贝的引用所指向的对象进行操作。因此,他和 C++ 的引用传递是是不一样的。
因此,Java 本至上还是值传递
结论
- 一个方法不能修改一个基本数据类型的参数
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象