《编程导论(Java)·3.3.2 按值传递语义》

不要受《Java编程思想》的影响,计算机科学中的术语——按引用传递(pass-by-reference),不要搞成自说自话的个人用语。这些术语也不是专门针对Java的,你不应该从某一本Java书上学习 不能够用于C、C++或Fortran语言的 特殊的“按引用传递”。

验证按值传递非常简单,在方法体中使用一个赋值语句,将形参作为左值。按值传递时,对形参的赋值,不会影响实参,也就是说,那个赋值语句不会有任何副作用。

对于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)调用了形参的方法,则导致形参指向的对象(也正是实参指向的对象)的内容改变,因而产生副作用。

为了避免方法调用可能带来的副作用,可以采用如下措施:

²       让引用指向的对象属于不变类;不变类的对象(内容)不可改变,如String。

²       克隆一个对象,将它的引用传递给方法。

 

3. 负负得正

有时候两个错误放在一起,从效果上看是正确的。典型的错误例子“Java中的对象按引用传递”。介绍这个错误的说法有两个目的:(1)说明什么是按引用传递;(2)强调当引用为方法参数时,传值仍然会有副作用。→

按引用传递意味着:方法的形式参数仅仅是实际参数的别名——实参不是将自己的值而是地址传递给形参,两者拥有相同的数据存放位置。因此任何时候方法改变形式参数的值,事实上也就改变了实参的值。

之所以有“Java中的对象按引用传递”这一负负得正的错误来源于一句容易令人误解的话:对象是通过引用传递的(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,各种参数传递的语义、按引用传递的目的.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值