引用类型的变量不直接包含其数据;它包含的是对其数据的引用。当通过值传递引用类型的参数时,有可能更改引用所指向的数据,如某类成员的值(更改属性的值),但是无法更改引用本身的值;也就是说,不能使用相同的引用为新类分配内存(比如在被调用的方法中通过new来分配新的内存空间)并使之在块外(调用方法中,比如Main方法中)保持。若要这样做,应使用引用传递方式(注意:引用传递方式和引用类型是不同的概念)——用 ref 或 out 关键字传递参数(参数类型可以是值类型也可以是引用类型)。为了简单起见,下面的示例使用 ref。
通过值传递引用类型
下面的示例演示通过值向 Change 方法传递引用类型的参数 arr。由于该参数是对 arr 的引用,所以有可能更改数组元素的值。但是,尝试将参数重新分配到不同的内存位置时,该操作仅在方法内有效,并不影响原始变量 arr。
class PassingRefByVal { static void Change(int[] pArray) { pArray[0] = 888; // 这里赋值 会改变 原来pArray[0]的值 pArray = new int[5] { -3, -1, -2, -3, -4 }; // 这里的赋值,只在该方法内有效 System.Console.WriteLine("在Change方法里面,arr第一个元素的值是:{0}", pArray[0]); pArray[1] = 666; // 这里赋值 不会改变 原来pArray[1]的值,只是改变这里新创建的数组的第一个元素的值 System.Console.WriteLine("在Change方法里面,arr第二个元素的值是:{0}", pArray[1]); } static void Main() { int[] arr = { 1, 4, 5 }; System.Console.WriteLine("在Main方法里面,调用Change方法前,arr数组中第一个元素的值是:{0}", arr[0]); System.Console.WriteLine("在Main方法里面,调用Change方法前,arr数组中第二个元素的值是:{0}", arr[1]); Change(arr); System.Console.WriteLine("在Main方法里面,调用Change方法后,第一个元素的值是:{0}", arr[0]); System.Console.WriteLine("在Main方法里面,调用Change方法后,第二个元素的值是:{0}", arr[1]); System.Console.ReadLine(); } } /* 输出: 在Main方法中,调用Change方法之前,pArray中第一个元素的值是:1 在Main方法中,调用Change方法之前,pArray中第二个元素的值是:4 在Change方法中,pArray中第一个元素的值是:-3 在Change方法中,pArray中第二个元素的值是:666 在Main方法中,调用Change方法后,pArray中第一个元素的值是:888 在Main方法中,调用Change方法后,pArray中第二个元素的值是:4 */
在上面的示例中,数组 arr 为引用类型,在未使用 ref 参数的情况下传递给方法。在此情况下,将向方法传递 指向 arr 的引用的一个副本 。输出显示方法有可能更改数组元素的内容,在这种情况下,从 1 改为 888。但是,在 Change 方法内使用 new 运算符来分配新的内存空间,将使变量 pArray 引用新的数组。因此,这之后的任何更改都不会影响原始数组 arr(它是在 Main 内创建的)。实际上,本示例中创建了两个数组,一个在 Main 内,一个在 Change 方法内。
下面的示例与前面的示例相比,除了在方法标头和调用方法的地方添加了 ref 关键字之外,其他的都一样。在方法中做的更改会影响到原始的变量。
class PassingRefByRef { static void Change(ref int[] pArray) { // 下面的更改都会影响到变量的原始值 pArray[0] = 888; pArray = new int[5] { -3, -1, -2, -3, -4 }; System.Console.WriteLine("在Change方法中,pArray中第一个元素的值是:{0}", pArray[0]); } static void Main() { int[] arr = { 1, 4, 5 }; System.Console.WriteLine("在Main方法中,调用Change方法之前,pArray中第一个元素的值是:{0}", arr[0]); Change(ref arr); System.Console.WriteLine("在Main方法中,调用Change方法后,pArray中第一个元素的值是:{0}", arr[0]); System.Console.ReadLine(); } } /* Output: 在Main方法中,调用Change方法之前,pArray中第一个元素的值是: 1 在Change方法中,pArray中第一个元素的值是: -3 在Main方法中,调用Change方法后,pArray中第一个元素的值是: -3 */
方法内发生的所有更改都影响 Main 中的原始数组。实际上,使用 new 运算符对原始数组对象进行了重新分配内存的操作。因此,调用 Change 方法后,对 arr 的任何引用都将指向 Change 方法中创建的新的数组。
交换字符串是传递引用类型参数的很好的示例。
1. 通过值传递:worker 和 manager 两个字符串在 Main 中进行初始化,并作为一般参数传递给静态方法 swap。这两个字符串在 swap 方法内进行了交,但在 Main 内没有进行交换。
class SwapEmployees { // 传值 static void swap(string e1, string e2) { string temp = e1; e1 = e2; e2 = temp; System.Console.WriteLine("在swap方法里:{0} {1}", e1, e2); } static void Main() { string worker = "John"; string manager = "Smith"; System.Console.WriteLine("在Main方法里,调用swap方法之前:{0} {1}", worker, manager); swap(worker, manager); // 通过引用传递方式,来传递字符串变量 System.Console.WriteLine("在Main方法里,调用swap方法之后:{0} {1}", worker, manager); System.Console.ReadLine(); } } /* Output: 在Main方法里,调用swap方法之前:John Smith 在swap方法里:Smith John 在Main方法里,调用swap方法之后:John Smith */
通过值传递时,对应的内存变化如图一所示:
图一:通过值传递引用类型参数时的内存变化
2. 通过引用传递:worker 和 manager 两个字符串在 Main 中进行初始化,并作为参数(带 ref 关键字)传递给静态方法 swap。这两个字符串在该方法内和 Main 内均进行了交换。
class SwapEmployees { // 传引用 static void swap(ref string e1, ref string e2) { string temp = e1; e1 = e2; e2 = temp; System.Console.WriteLine("在swap方法里:{0} {1}", e1, e2); } static void Main() { string worker = "John"; string manager = "Smith"; System.Console.WriteLine("在Main方法里,调用swap方法之前:{0} {1}", worker, manager); swap(ref worker, ref manager); // 通过引用传递方式,来传递字符串变量 System.Console.WriteLine("在Main方法里,调用swap方法之后:{0} {1}", worker, manager); System.Console.ReadLine(); } } /* Output: 在Main方法里,调用swap方法之前:John Smith 在swap方法里:Smith John 在Main方法里,调用swap方法之后:Smith John */
通过引用传递时,对应的内存变化如图二所示:
图二:通过引用传递引用类型参数时的内存变化
总结:
本示例中,需要通过传递引用的方式来传递参数,才能影响调用程序中的对象。如果把方法声明和方法调用中的 ref 关键字都去掉,那么调用方法后,原对象不会被重新分配内存。不难发现,上面的两种方式与值类型和引用类型的存储方式密切相关。关于值类型和引用类型的存储方式的区别,可以参考文章:http://www.xuexila.com/baikezhishi/536966.html
//摘录其中一段 与 C++ 不同,Java 自动管理栈和堆,程序员不能直接地设置栈或堆。 Java 的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过 new, newarray, anewarray, multianewarray 等指令建立,它们不需要程序代码来显式的释放。堆由垃圾回收来负责,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。 栈的优势是,存取速度比堆要快,仅次于寄存器,栈的数据还可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。 栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义: int a = 3; int b = 3; 编译器先处理 int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理 int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了 a 与 b 同时均指向3的情况。这时,如果再令 a = 4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令 a 指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况 a 的修改并不会影响到 b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量 。
官方文档: