曾经在面试中被问到一个问题:”Java中传递方式是值传递还是引用传递?”其实我个人觉得不必过于纠结于这种字面上的意义,而应该从本质上去理解Java在数据传递过程中所发生的变化。
众所周知,Java的数据类型分为基本类型和引用类型,其中基本类型就是int/double/char…,而引用类型则是指类、接口、String类型。两者的主要区别如下:
int num = 10;
Student stu = new Student();
如图所示,num是基本类型,因此值直接保存在变量中,而stu是引用类型,变量中保存的是实际Student对象的地址,即所谓的“引用”,引用指向实际对象,实际对象中保存着内容。参数的传递本质上就是赋值(=)操作。
对于基本类型num,进行赋值操作时会直接覆盖变量的值。
对于引用类型stu,进行赋值操作实际上是改变变量中所保存的地址值,而实际对象是不会被改变的。
我们可以通过一些例子来分析Java的参数传递,主要以传递的变量是基本类型和引用类型进行区分:
1、传递参数为基本类型时:
public class ParamTest {
public static void main(String[] args){
int num = 10;
changeNum(num);
System.out.println(num);
}
public static void changeNum(int temp){
temp = temp * 2;
}
}
输出结果:10
从图中看出,temp的值改变为了原先的2倍,而num的值没有变化,说明以基本类型作为参数传递时,实际上传递的只是一份数值的拷贝,传入函数以后就跟原来的值没有关系了,在函数中改变这个数的值也不会影响原来变量的值。
2、传递参数为引用类型时:
当传递的参数类型为引用类型时,是我们比较容易判断出错的情况。其实我们只要牢记一点就好了,就是传递的引用类型并不是对象本身,而是指向对象的地址,我们可以通过绘制内存指向图来分析判断。下面将通过三段代码来进行举例说明。
public class ParamTest {
public static void main(String[] args){
Student a = new Student();
a.score = 80;
changeScore(a);
System.out.println(a.score);
}
public static void changeScore(Student stu){
stu.score = 90;
}
}
class Student{
public int score;
}
输出结果为:90
由图可以明白,stu与a指向同一个Student对象,对stu指向对象进行操作,也是对a指向的同一个对象进行操作,因此输出分数发生了变化
public class ParamTest {
public static void main(String[] args){
Student a = new Student();
a.score = 80;
changeScore(a);
System.out.println(a.score);
}
public static void changeScore(Student stu){
stu = new Student(); //增加的一行代码
stu.score = 90;
}
}
class Student{
public int score;
}
输出结果为:80
这个例子与上一个例子仅增加了一行代码,而输出则发生了变化,由图可以很清晰看出,在执行了stu = new Student()以后,stu地址发生了变化,指向了一个新的Student对象,因此对stu指向的对象进行操作时,并不影响a指向的对象的score。
public class ParamTest {
public static void main(String[] args){
Student a = new Student();
Student b = new Student();
a.score = 80;
b.score = 100;
System.out.println("交换前:");
System.out.println("a的分数:"+ a.score + "---b的分数: "+ b.score);
changeScore(a,b);
System.out.println("交换后:");
System.out.println("a的分数:"+ a.score + "---b的分数: "+ b.score);
}
public static void changeScore(Student x,Student y){
Student stu = x;
x = y;
y = stu;
System.out.println("交换后:");
System.out.println("x的分数:"+ x.score + "---y的分数: "+ y.score);
}
}
class Student{
public int score;
}
输出结果为:
交换前:
a的分数:80—b的分数: 100
交换后:
x的分数:100—y的分数: 80
交换后:
a的分数:80—b的分数: 100
从图中可以发现,在执行交换函数后,仅仅是x和y的地址发生了交换,并不影响原先a和b指向的对象。
3、String不可变类:
此外还有一种比较特殊情况需要注意,就是String类型对象,虽然其也是引用类型,下面先看一个例子。
public class ParamTest {
public static void main(String[] args){
String s = new String("XZ");
StringBuilder sb = new StringBuilder("XZ");
change(s,sb);
System.out.println(s);
System.out.println(sb);
}
public static void change(String str,StringBuilder sb){
str = "XZ Premium";
sb.append("Premium");
}
}
输出结果为:
XZ
XZPremium
其中一点让人比较奇怪的是String作为引用类型,为什么在子函数对其进行操作,并没有改变原先String对象的值。接下来我们看下内存指向图。
由于String是final修饰的不可变类,因此对String进行的操作都会重新分配一块内存生成一个新的String对象,并不影响原先的String对象。
4、总结:
经过以上的分析,关于Java中的值传递和引用传递可以得出以下结论:
(1) 基本数据类型传值,对形参的修改不会影响实参;
(2) 引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象;
(3) String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。