无论是什么语言,要讨论参数传递方式,就得从内存模型说起,主要是我个人觉得从内存模型来说参数传递更为直观一些。闲言少叙,下面我们就通过内存模型的方式来讨论一下Java中的参数传递。

这里的内存模型涉及到两种类型的内存:栈内存(stack)和堆内存(heap)。基本类型作为参数传递时,传递的是这个值的拷贝。无论你怎么改变这个拷贝,原值是不会改变的。看下边的一段代码,然后结合内存模型来说明问题:

public class ParameterTransfer {
 public static void main(String[] args) {
  int num = 30;
  System.out.println("调用add方法前num=" + num);
  add(num);
  System.out.println("调用add方法后num=" + num);
 }
 
 public static void add(int param) {
  param = 100;
 }
 
}

 

    这段代码运行的结果如下:

调用add方法前num=30
调用add方法后num=30

    程序运行的结果也说明这一点,无论你在add()方法中怎么改变参数param的值,原值num都不会改变。

    下边通过内存模型来分析一下。

    当执行了int num = 30;这句代码后,程序在栈内存中开辟了一块地址为AD8500的内存,里边放的值是30,内存模型如下图:

Java参数传递 java参数传递内存_栈内存

    执行到add()方法时,程序在栈内存中又开辟了一块地址为AD8600的内存,将num的值30传递进来,此时这块内存里边放的值是30,执行param = 100;后,AD8600中的值变成了100。内存模型如下图:   

Java参数传递 java参数传递内存_参数传递_02

    地址AD8600中用于存放param的值,和存放num的内存没有任何关系,无论你怎么改变param的值,实际改变的是地址为AD8600的内存中的值,而AD8500中的值并未改变,所以num的值也就没有改变。
    以上是基本类型参数的传递方式,下来我们讨论一下对象作为参数传递的方式。

    先看下边的示例代码:

public class ParameterTransfer {
 public static void main(String[] args) {
  String[] array = new String[] {"huixin"};
  System.out.println("调用reset方法前array中的第0个元素的值是:" + array[0]);
  reset(array);
  System.out.println("调用reset方法后array中的第0个元素的值是:" + array[0]);
 }
 
 public static void reset(String[] param) {
  param[0] = "hello, world!";
 }
 
}

运行的结果如下:

调用reset方法前array中的第0个元素的值是:huixin
调用reset方法后array中的第0个元素的值是:hello, world!

当对象作为参数传递时,传递的是对象的引用,也就是对象的地址。下边用内存模型图来说明。

Java参数传递 java参数传递内存_JAVA_03

当程序执行了String[] array = new String[] {"huixin"}后,程序在栈内存中开辟了一块地址编号为AD9500内存空间,用于存放array[0]的引用地址,里边放的值是堆内存中的一个地址,示例中的值为BE2500,可以理解为有一个指针指向了堆内存中的编号为BE2500的地址。堆内存中编号为BE2500的这个地址中存放的才是array[0]的值:huixin。

当程序进入reset方法后,将array的值,也就是对象的引用BE2500传了进来。这时,程序在栈内存中又开辟了一块编号为AD9600的内存空间,里边放的值是传递过来的值,即AD9600。可以理解为栈内存中的编号为AD9600的内存中有一个指针,也指向了堆内存中编号为BE2500的内存地址,如图所示:

Java参数传递 java参数传递内存_JAVA_04

这样一来,栈内存AD9500和AD9600(即array[0]和param的值)都指向了编号为BE2500的堆内存。

在reset方法中将param的值修改为hello, world!后,内存模型如下图所示:

Java参数传递 java参数传递内存_栈内存_05

改变对象param的值实际上是改变param这个栈内存所指向的堆内存中的值。param这个对象在栈内存中的地址是AD9600,里边存放的值是BE2500,所以堆内存BE2500中的值就变成了hello,world!。程序放回main方法之后,堆内存BE2500中的值仍然为hello,world!,main方法中array[0]的值时,从栈内存中找到array[0]的值是BE2500,然后去堆内存中找编号为BE2500的内存,里边的值是hello,world!。所以main方法中打印出来的值就变成了hello,world!

无论是基本类型作为参数传递,还是对象作为参数传递,实际上传递的都是值,只是值的的形式不用而已。第一个示例中用基本类型作为参数传递时,将栈内存中的值30传递到了add方法中。第二个示例中用对象作为参数传递时,将栈内存中的值BE2500传递到了reset方法中。当用对象作为参数传递时,真正的值是放在堆内存中的,传递的是栈内存中的值,而栈内存中存放的是堆内存的地址,所以传递的就是堆内存的地址。这就是它们的区别。

首先,不要纠结于 Pass By Value 和 Pass By Reference 的字面上的意义,否则很容易陷入所谓的“一切传引用其实本质上是传值”这种并不能解决问题无意义论战中。
更何况,要想知道Java到底是传值还是传引用,起码你要先知道传值和传引用的准确含义吧?可是如果你已经知道了这两个名字的准确含义,那么你自己就能判断Java到底是传值还是传引用。
这就好像用大学的名词来解释高中的题目,对于初学者根本没有任何意义。

一:搞清楚 基本类型 和 引用类型的不同之处

int num = 10;
String str = "hello";


如图所示,num是基本类型,值就直接保存在变量中。而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。

Java参数传递 java参数传递内存_参数传递_06

如图所示,num是基本类型,值就直接保存在变量中。而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。


二:搞清楚赋值运算符(=)的作用

num = 20;
str = "java";

对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。

Java参数传递 java参数传递内存_堆内存_07

对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。

对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。

如上图所示,"hello" 字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)


三:调用方法时发生了什么?参数传递基本上就是赋值操作

第一个例子:基本类型
void foo(int value) {
    value = 100;
}
foo(num); // num 没有被改变

第二个例子:没有提供改变自身方法的引用类型
void foo(String text) {
    text = "windows";
}
foo(str); // str 也没有被改变

第三个例子:提供了改变自身方法的引用类型
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
    builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。

第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
    builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。


重点理解为什么,第三个例子和第四个例子结果不同?


下面是第三个例子的图解:

builder.append("4")之后

Java参数传递 java参数传递内存_JAVA_08

builder.append("4")之后

下面是第四个例子的图解:

Java参数传递 java参数传递内存_参数传递_09

下面是第四个例子的图解:


Java参数传递 java参数传递内存_JAVA_08


builder = new StringBuilder("ipad"); 之后