不要受《Java编程思想》的影响,计算机科学中的术语——按引用传递(pass-by-reference)。不要搞成自说自话的个人用语。
这些术语也不是专门针对Java的,你不应该从某一本Java书上学习 不可以用于C、C++或Fortran语言的 特殊的“按引用传递”。
验证按值传递很easy。在方法体中使用一个赋值语句,将形參作为左值。
按值传递时,对形參的赋值,不会影响实參。也就是说。那个赋值语句不会有不论什么副作用。
对于foo(A a),注意方法体中你要玩的是 a= new A(),而不是玩还有一个东西。如a.change()。
这段文字秒懂的,《编程导论(Java)·3.3.2 按值传递语义》的内容能够跳过。
通过定义一系列方法,能够将程序分解成小模块,而方法调用将它们联系起来。方法定义时指定了形式參数;而在方法调用时,形式參数由给定的实际參数初始化。
消息传递中的一个重要议题是:消息參数(实參)应该怎样传递给方法的形參?在各种编程语言中。參数传递的方式多种多样[1]。这由语言的设计者和实现者取舍。
经常使用的參数传递的方式有按值传递(pass-by-value)和按引用传递(pass-by-reference)。
从參数传递机制的渊源上看,C语言中的參数是按值传递的,Fortran语言按引用传递,而C++语言中同一时候採用了两者。Java语言与C语言一样,採用唯一的參数传递方式:按值传递。
參数化机制须要考虑两个问题:
形參初始化。
方法体中对形參的操作是否对实參产生副作用。
1. 方法调用栈
按值传递意味着:当调用某个方法时,首先实际參数(或表达式)被求值,(并将结果值进行复制。再)把复制的值存放到形式參数中。
简言之,按值传递就是传递实际參数的一个副本。
原理上看(方法栈帧在[7.4执行时存储管理]中具体解释),Java每调用一次方法,就创建一个新的方法帧。
形式參数(无论是基本类型还是引用类型变量)属于自己的方法帧,形參保存其值的空间在栈上分配。
而实际參数(或表达式)既可能在heap中(对象的域),也可能属于还有一个方法帧(还有一个局部变量),两者是独立的。按值传递时,假设被调用的方法改动了形參的值,仅改变了副本,而(实參的)原始值丝毫不受影响。
例程 3?13方法调用 package OO; import static tips.Print.*; public class PassByValue{ private void m(int x){ x += 5; } private int max(int a,int b){ return ( a>b ? a : b ); } public void foo(){ ? int i = 1,j =2;//代码前的符号,表示断点 int max = max(i,j); m(max); i=max; } }
创建一个对象并运行其foo()方法,foo()的运行过程,如图3-6所看到的。
它反映了两个要点:(1)一个“较大的代码”怎样分解成较多的小片段(方法),而后这些小片段又是怎样构成一个大总体的——假想方法m(int )和max (int,int)有着非常长非常长的代码。
(2)方法调用的运行流程。
图3?6 方法调用流程
foo()的运行过程:(1) 初始化局部变量i和j;(2) int max = max(i,j)。先求方法max(i,j)的(返回)值,然后赋值给局部变量max。
为了求方法max(i,j)的值。JVM创建一个新的方法帧max,将上一帧foo的局部变量i和j的值复制后赋予形參,foo帧处于等待状态。
max运行完成将返回2,max帧被弹出,2赋值给max;(3)运行m(max)。创建新帧m,将帧foo的实參max的值2复制后赋予形參x。m帧尽管改变了x的值。可是不影响实參的值。
假设在学习[2.3.4创建对象]的时候,熟悉了在BlueJ的源码编辑器中设置断点,则能够在如图3-7所看到的的方法帧调用栈中,在两个帧间切换以观察实參与形參分别在各自帧中分配有自己的空间。
图 3?7 在两个帧间切换
2. Java语言中仅仅有按值传递
学习Java语言的參数传递方式,要验证3种情况:
(1)对于基本类型的參数。方法体中对形參的操作不会产生副作用。
(2)以对象的引用作为參数时。实參(引用)相同不会改变。
(3)可是将该引用作为消息接收者,可能使它指向的对象的内容发生了变化。
package OO; import tips.Fraction; import static tips.Print.*; public class PassByValue{ /////////////////////////////////////以引用作为參数。仍然按值传递//////// private void change(Fraction frrr) { frrr = new Fraction(11,55);//注意这里。 } private void doubleIt(Fraction f) { f.add(f); } public void test(){ Fraction f = new Fraction(1,3); p(f+" "); change(f); pln(f); //f = 1/3 Vs 1/5 Fraction f2 = new Fraction(1,3); //Fraction temp = new Fraction(f2); doubleIt(f2); //doubleIt(temp); pln (f2); } }
例程中,change(Fraction)和doubleIt(Fraction) 方法以分数类变量为形參。运行test()代码可知,change(Fraction)对形參的赋值不会影响实參。而doubleIt(Fraction)调用了形參的方法,则导致形參指向的对象(也正是实參指向的对象)的内容改变,因而产生副作用。
为了避免方法调用可能带来的副作用,能够採用例如以下措施:
2 让引用指向的对象属于不变类。不变类的对象(内容)不可改变,如String。
2 克隆一个对象,将它的引用传递给方法。
3. 负负得正
有时候两个错误放在一起,从效果上看是正确的。典型的错误样例“Java中的对象按引用传递”。介绍这个错误的说法有两个目的:(1)说明什么是按引用传递;(2)强调当引用为方法參数时。传值仍然会有副作用。
→
按引用传递意味着:方法的形式參数不过实际參数的别名——实參不是将自己的值而是地址传递给形參,两者拥有同样的数据存放位置。因此不论什么时候方法改变形式參数的值。其实也就改变了实參的值。
之所以有“Java中的对象按引用传递”这一负负得正的错误来源于一句easy令人误解的话:对象是通过引用传递的(you are passing objects by reference)。其本意是说。Java中的对象不被传递,而是传递其引用。可是不论是英文还是中文的含义,稍不小心就会与pass-by-reference混淆。所谓负负得正,基于:
(1) 对象可以传递。Java中不会传递对象。所以这是错误的如果。根源是由于人们经常混用术语。“把对象传递给方法”毕竟是经常使用的说法,见[2.4.2引用变量、引用和对象]。
(2) 形參和实參拥有同样的位置。
假设形參和实參都是对象。这当然是对的。问题是,形參和实參(不是对象而)是引用,正如左手和右手指向同一个月亮,可是左手不是右手,左手不是右手的别名/外号。
效果正确:的确可以改动对象的内容。
总之,正确的说法是对象的引用按值传递(Object references are passed by value)。
练习3-1.:或许有人说,“对象是通过引用传递。而引用按值传递”这句话太绕口,没有“对象按引用传递”来得明快。你怎样回答? |
练习3-2.:为什么说“基本类型按值传递,而引用使用按引用传递”是错误的。 |
练习3-3.:网络程序中传递序列化的对象,应该採用什么传递机制?提示:传引用语意。 |
[1] http://www.yoda.arachsys.com/java/passing.html,各种參数传递的语义、按引用传递的目的.
《编程导论(Java)·3.3.2 按值传递语义》