1. 程式人生 > >《編程導論(Java)·3.3.2 按值傳遞語義》

《編程導論(Java)·3.3.2 按值傳遞語義》

文字 指定 來源 center public 語言 斷點 enter art

不要受《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 按值傳遞語義》