背景

Java 是值传递还是引用传递?这个问题一直以来都有所争议。查阅了很多资料,结果显示绝大多数的观点都倾向于 Java 是值传递的,也有一部分观点的关注点在题目本身是否合理,还有一小部分人可能在看了很多的相关资料后更加的迷惑了,也根据自己的理解提出了质疑。

俗话说的好,一千个人眼里有一千个哈姆雷特,也不必太过纠结,我仅在这篇文章整理分析一下最多的一个观点:Java中只有值传递。整理下这篇文章主要是为了让自己记忆的更深刻,当然如果发布出来对看到的人有一点点帮助也是极好的。

定义

先看一个简单的例子1

// 例1
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 20;
change(a, b);
System.out.println(a);
System.out.println(b);
}

public static void change (int c, int d){
c = 50;
d = 60;
}

}

输出结果:

传递之前:10, 20

传递之后:10, 20

也就是a、b的值传递给 change 方法之后原值并没有被改变,这种传递就被称为是值传递,如果原值在经过 change 方法后被改变了,就是引用传递了。

  • 值传递:值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  • 引用传递:引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

分析

定义里面的例子1得出Java是值传递的结果,但是可能会有人拿出以下的例子2来进行反驳:

// 例2
public class Main {

public static void main(String[] args) {
User user = new User();
user.setName("小胖");
user.setAge(23);
System.out.println("转换之前:" + user.toString());
change(user);
System.out.println("转换之后:" + user.toString());
}

public static void change (User user){
user.setName("小黑");
user.setAge(25);
}

}

运行结果:

传递之前:User{name=‘小胖’, age=‘23’}

传递之后:User{name=‘小黑’, age=‘25’}
同样的一个 change 方法,同样的执行流程,实参的值却被改变了,按照上面的定义,这不就是引用传递嘛。可能就会有人得出自己的结论了:Java的方法中,在传递基本数据类型的时候是值传递,在传递引用数据类型的时候是引用传递。

这种结论仍然是错误的,请看下面这个例子3

// 例3
public class Main {

public static void main(String[] args) {
String name = "小胖";
System.out.println("转换之前:" + name);
change(name);
System.out.println("转换之后:" + name);
}

public static void change (String name){
name = "小黑";
}

}

运行结果:

转换之前:小胖

转换之后:小胖

这又是怎么回事呐?String 同样是引用数据类型,为什么传递之后,原始值并没有被改变呐。


堆内存、栈内存

上面三个例子可能就是让人产生困惑的原因之一。要弄明白上面的三个例子为什么会产生相应结果的原因就要引入 Java 中的堆内存栈内存的相关知识了:

  • 堆内存用来存放由new创建的对象和数组。
  • 在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量

对比二者的特点:

存储区域

存储内容

优点

缺点

回收


基本类型的变量和对象的引用

存取速度比堆要快,仅次于寄存器,栈数据可以共享

存在栈中的数据大小与生存期必须是确定的,缺乏灵活性

当超过变量的作用域后,Java会自动释放掉该变量


由new等指令创建的对象和数组

可以动态地分配内存大小,生存期也不必事先告诉编译器

要在运行时动态分配内存,存取速度较慢

由Java虚拟机的自动垃圾回收器来回收不再使用的数据

引用变量就相当于是为数组或对象起的一个名称,然后就可以使用这个在栈中的名称(引用变量)来访问堆中的数组或对象。

了解了堆内存和栈内存的相关知识后,我们就可以对上面的例子做出解释了:

例2中创建的 User 对象 User user = new User(),实际经过了这几个步骤:

User user;    // 在栈内存里面开辟了空间给引用变量user,此时user=null
user = new User();
// 1. new User() 在堆内存里面开辟了空间给 User 类的对象,这个对象没有名字
// 2. User() 随即调用了 User 类的构造函数
// 3. 把创建的 User 对象在堆内存中的地址给在栈内存的引用变量 user

到这里应该差不多就应该明白了引用类型是怎么传递的了,

public class Main {

public static void main(String[] args) {
User user = new User();
user.setName("小胖");
user.setAge(23);
System.out.println("转换之前:" + user.toString());
change(user); // 此处传递的是对象在栈内存中的引用地址
System.out.println("转换之后:" + user.toString());
}

public static void change (User user){
user.setName("小黑");
user.setAge(25);
}

}

change(user); 传递的值其实就是对象在栈内存中的引用,这个引用本身也是一个值,然后改变这个引用指向的对象的具体属性值,就等于是在操作原来的对象。所以例2的原值会被改变。

观察下面这个例子4

// 例 4
public class Main {

public static void main(String[] args) {
User user = new User();
user.setName("小胖");
user.setAge(23);
System.out.println("转换之前:" + user.toString());
change(user);
System.out.println("转换之后:" + user.toString());
}

public static void change(User user) {
user = new User();
user.setName("小黑");
user.setAge(25);
}

}

运行结果:

转换之前:User{name=‘小胖’, age=‘23’}

转换之后:User{name=‘小胖’, age=‘23’}

在 change 方法中添加了一行代码 user = new User(),相信小伙伴可以预见到这个运行结果。

user = new User(); 相当于引用变量指向了一个 new 出来的新的对象,此时的 user 已经不是 main 方法中原来对象的引用了,它已经和 main 方法中的 user 没有关系了,它改变自己指向的对象的属性值自然就不会影响到原来的对象了。

同时,这个例子也解释了上面的例3,

// 例3
public class Main {

public static void main(String[] args) {
String name = "小胖";
System.out.println("转换之前:" + name);
change(name);
System.out.println("转换之后:" + name);
}

public static void change (String name){
name = "小黑"; // 这里等同于 name = new String("小黑");
}

}

其实 change 方法中的 name = “小黑”,就相当于 name = new String(“小黑”); String 类型是 final 修饰的,不能被改变的,前面两种只是写法不同。

补充一个例子5

// 例5
public class Main {

public static void main(String[] args) {
StringBuffer s = new StringBuffer("小胖");
System.out.println("转换之前:" + s.toString());
change(s);
System.out.println("转换之后:" + s.toString());
}

public static void change(StringBuffer s) {
s.append(23);
}

}

运行结果:

转换之前:小胖

转换之后:小胖23

这个 change 中的 s 和 main 中的 s 还是指向的同一个对象,StringBuffer 没有 final 修饰,是可以被改变的。执行 append 方法,原来的字符串也同样会改变。

引用《Java编程思想》中的一句话:

倘若“将一个对象赋值给另一个对象”,实际是将“引用”从一个地方复制到另一个地方。


如有错误,欢迎指正。