为什么说 Java 只有值传递
结论:不管是基本数据类型还是引用类型 reference ,Java 中都是值传递,在函数中对引用类型参数所进行修改,是否会影响到实际参数。需要从是否是操作的同一个对象这个角度进行分析。
引用传递和值传递的概念
值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
在 Java 中一般认为基本数据类型、基本数据类型的包装类和 String 是值传递,引用类型是引用传递。
public class Test {
static void test(int var1, Integer var2, String var3, Map var4) {
var1 = 2;
var2 = 2;
var3 = var3 + "2";
var4.put("2", "2");
}
public static void main(String[] args) {
int var1 = 1;
Integer var2 = 1;
String var3 = "1";
Map var4 = new HashMap();
var4.put("1", "1");
test(var1, var2, var3, var4);
System.out.println("var1(int):" + var1);
System.out.println("var2(Integer):" + var2);
System.out.println("var3(String):" + var3);
System.out.println("var4(HashMap):" + var4);
// 输出为
// var1(int):1
// var2(Integer):1
// var3(String):1
// var4(HashMap):{1=1, 2=2}
}
}
参数在传递到 test 方法中赋值后,在 main 方法中看只有 var4 (HashMap) 的值受到了影响。这样看来,确实是值类型是值传递,引用类型是引用传递。但是并非如此。
方法执行的内存模型是 Java 虚拟机栈,在每个方法被执行的时候都会创建一个栈桢,方法的参数和局部变量都保存在栈桢中的局部变量表中。
局部变量表的容量以局部变量槽为单位,每个局部变量槽能存放一个 boolean、byte、char、short、int、float、reference 和 returnAddress类型的数据 (Java 基本数据类型中的其他两种 long 和 double 都会占用两个局部变量槽) 。
引用类型在方法栈中只保存了 reference ,真正的对象在 Java 堆中。无论 Java 虚拟机是使用哪种方式通过 reference 定位对象 (主要有使用句柄和直接指针两种) ,都可以把 reference 理解为对象在堆中的地址。
在刚进入 test 方法的时候方法中的 var3 和 main 方法中的 var3 是指向堆中的同一个 String 对象,在运行完 test 方法最后一行的时候 Java 虚拟机内存模型是这样的。
同样是 reference 类型的 var3 和 var4,为何经过了 test 方法后只有 var4 的值改变了。其实 test 方法执行完第三行 var3 = var3 + “2” 后,main 方法中的 var3 和 test 方法中的 var3 已经不是指向 Java 堆中的同一个对象了。也就是在 test 方法中 reference3 的值 (字符串对象在堆中的地址) 已经发生了改变。但是未影响到 main 方法中的 reference1的值。
在 reference 类型参数传递的时候,也是值传递,复制了一份 reference (对象的地址) 传递到了 test 方法的栈桢局部变量表中,在复制的 reference 发生改变的时候,不会影响到原方法中的 reference 值。reference 的传递也是值传递,所以在 Java 中,参数传递只有值传递,没有引用传递。
为何 var4(HashMap) 还指向同一个对象,但是 var3(String) 却不指向同一个对象了呢。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
在 String 类中保存字符串的 char 数组用了 final 修饰。char 数组不能重新赋值,所以字符串的值也就不能改变。其实 test 中的 var3 = var3 + “2” 语句创建了一个新的 String 对象,等同于以下写法。
static void test(int var1, Integer var2, String var3, Map var4) {
var1 = 2;
var2 = 2;
// var3 指向新的对象
var3 = new String(var3 + "2");
var4.put("2", "2");
}
要是 把 String 换成 StringBuilder,结果将会改变。
public class Test {
static void test(int var1, Integer var2, StringBuilder var3, Map var4) {
var1 = 2;
var2 = 2;
var3.append("2");
var4.put("2", "2");
}
public static void main(String[] args) {
int var1 = 1;
Integer var2 = 1;
StringBuilder var3 = new StringBuilder("1");
Map var4 = new HashMap();
var4.put("1", "1");
test(var1, var2, var3, var4);
System.out.println("var1(int):" + var1);
System.out.println("var2(Integer):" + var2);
System.out.println("var3(StringBuilder):" + var3);
System.out.println("var4(HashMap):" + var4);
// 输出为
// var1(int):1
// var2(Integer):1
// var3(String):12
// var4(HashMap):{1=1, 2=2}
}
}
因为 StringBuilder 继承 AbstractStringBuilder,AbstractStringBuilder 中保存字符串的 char 数组没有使用 final 修饰。并且能对 char 数组进行修改和扩容,提供了 append 等方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
结论:不管是基本数据类型还是引用类型 reference ,Java 中都是值传递,在函数中对引用类型参数所进行修改,是否会影响到实际参数。需要从是否是操作的同一个对象这个角度进行分析。
public class Test {
static void test(Map var) {
var = new HashMap();
var.put("2", "2");
}
public static void main(String[] args) {
Map var = new HashMap();
var.put("1", "1");
test(var);
System.out.println("var:" + var);
// 输出为
// var:{1=1}
}
}
以上例子,test 中的进行 put 的 Map 和 main 方法中的已经不是同一个对象,所以对他操作不会影响 main 方法中 Map 的值,test 方法执行完成后其中创建的 Map 对象将不再是 GC Roots 且没有 GC Roots 引用他所以将会由垃圾收集器进行收集。