Java 應(yīng)用程序中的按值傳遞語(yǔ)義(轉(zhuǎn))
發(fā)表時(shí)間:2023-07-17 來(lái)源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]Java 應(yīng)用程序中的按值傳遞語(yǔ)義原文http://www.cn.ibm.com/developerWorks/java/passbyval/index.shtml節(jié)選理解參數(shù)是按值而不是按引用傳遞...
Java 應(yīng)用程序中的按值傳遞語(yǔ)義
原文http://www.cn.ibm.com/developerWorks/java/passbyval/index.shtml
節(jié)選理解參數(shù)是按值而不是按引用傳遞的說(shuō)明 Java 應(yīng)用程序有且僅有的一種參數(shù)傳遞機(jī)制,即按值傳遞。寫它是為了揭穿普遍存在的一種神話,即認(rèn)為 Java 應(yīng)用程序按引用傳遞參數(shù),以避免因依賴“按引用傳遞”這一行為而導(dǎo)致的常見編程錯(cuò)誤。
對(duì)此節(jié)選的某些反饋意見認(rèn)為,我把這一問題搞糊涂了,或者將它完全搞錯(cuò)了。許多不同意我的讀者用 C++ 語(yǔ)言作為例子。因此,在此欄目中我將使用 C++ 和 Java 應(yīng)用程序進(jìn)一步闡明一些事實(shí)。
要點(diǎn)
讀完所有的評(píng)論以后,問題終于明白了,至少在一個(gè)主要問題上產(chǎn)生了混淆。某些評(píng)論認(rèn)為我的節(jié)選是錯(cuò)的,因?yàn)閷?duì)象是按引用傳遞的。對(duì)象確實(shí)是按引用傳遞的;節(jié)選與這沒有沖突。節(jié)選中說(shuō)所有參數(shù)都是按值 -- 另一個(gè)參數(shù) -- 傳遞的。下面的說(shuō)法是正確的:在 Java 應(yīng)用程序中永遠(yuǎn)不會(huì)傳遞對(duì)象,而只傳遞對(duì)象引用。因此是按引用傳遞對(duì)象。但重要的是要區(qū)分參數(shù)是如何傳遞的,這才是該節(jié)選的意圖。Java 應(yīng)用程序按引用傳遞對(duì)象這一事實(shí)并不意味著 Java 應(yīng)用程序按引用傳遞參數(shù)。參數(shù)可以是對(duì)象引用,而 Java 應(yīng)用程序是按值傳遞對(duì)象引用的。
C++ 和 Java 應(yīng)用程序中的參數(shù)傳遞
Java 應(yīng)用程序中的變量可以為以下兩種類型之一:引用類型或基本類型。當(dāng)作為參數(shù)傳遞給一個(gè)方法時(shí),處理這兩種類型的方式是相同的。兩種類型都是按值傳遞的;沒有一種按引用傳遞。這是一個(gè)重要特性,正如隨后的代碼示例所示的那樣。
在繼續(xù)討論之前,定義按值傳遞和按引用傳遞這兩個(gè)術(shù)語(yǔ)是重要的。按值傳遞意味著當(dāng)將一個(gè)參數(shù)傳遞給一個(gè)函數(shù)時(shí),函數(shù)接收的是原始值的一個(gè)副本。因此,如果函數(shù)修改了該參數(shù),僅改變副本,而原始值保持不變。按引用傳遞意味著當(dāng)將一個(gè)參數(shù)傳遞給一個(gè)函數(shù)時(shí),函數(shù)接收的是原始值的內(nèi)存地址,而不是值的副本。因此,如果函數(shù)修改了該參數(shù),調(diào)用代碼中的原始值也隨之改變。
關(guān)于 Java 應(yīng)用程序中參數(shù)傳遞的某些混淆源于這樣一個(gè)事實(shí):許多程序員都是從 C++ 編程轉(zhuǎn)向 Java 編程的。C++ 既包含非引用類型,又包含引用類型,并分別按值和按引用傳遞它們。Java 編程語(yǔ)言有基本類型和對(duì)象引用;因此,認(rèn)為 Java 應(yīng)用程序像 C++ 那樣對(duì)基本類型使用按值傳遞,而對(duì)引用使用按引用傳遞是符合邏輯的。畢竟您會(huì)這么想,如果正在傳遞一個(gè)引用,則它一定是按引用傳遞的。很容易就會(huì)相信這一點(diǎn),實(shí)際上有一段時(shí)間我也相信是這樣,但這不正確。
在 C++ 和 Java 應(yīng)用程序中,當(dāng)傳遞給函數(shù)的參數(shù)不是引用時(shí),傳遞的都是該值的一個(gè)副本(按值傳遞)。區(qū)別在于引用。在 C++ 中當(dāng)傳遞給函數(shù)的參數(shù)是引用時(shí),您傳遞的就是這個(gè)引用,或者內(nèi)存地址(按引用傳遞)。在 Java 應(yīng)用程序中,當(dāng)對(duì)象引用是傳遞給方法的一個(gè)參數(shù)時(shí),您傳遞的是該引用的一個(gè)副本(按值傳遞),而不是引用本身。請(qǐng)注意,調(diào)用方法的對(duì)象引用和副本都指向同一個(gè)對(duì)象。這是一個(gè)重要區(qū)別。Java 應(yīng)用程序在傳遞不同類型的參數(shù)時(shí),其作法與 C++ 并無(wú)不同。Java 應(yīng)用程序按值傳遞所有參數(shù),這樣就制作所有參數(shù)的副本,而不管它們的類型。
示例
我們將使用前面的定義和討論分析一些示例。首先考慮一段 C++ 代碼。C++ 語(yǔ)言同時(shí)使用按值傳遞和按引用傳遞的參數(shù)傳遞機(jī)制:
清單 1:C++ 示例 #include
#include
void modify(int a, int *P, int &r);
int main (int argc, char** argv)
{
int val, ref;
int *pint;
val = 10;
ref = 50;
pint = (int*)malloc(sizeof(int));
*pint = 15;
printf("val is %d\n", val);
printf("pint is %d\n", pint);
printf("*pint is %d\n", *pint);
printf("ref is %d\n\n", ref);
printf("calling modify\n");
//按值傳遞 val 和 pint,按引用傳遞 ref。
modify(val, pint, ref);
printf("returned from modify\n\n");
printf("val is %d\n", val);
printf("pint is %d\n", pint);
printf("*pint is %d\n", *pint);
printf("ref is %d\n", ref);
return 0;
}
void modify(int a, int *p, int &r)
{
printf("in modify...\n");
a = 0;
*p = 7;
p = 0;
r = 0;
printf("a is %d\n", a);
printf("p is %d\n", p);
printf("r is %d\n", r);
}
這段代碼的輸出為:
清單 2:C++ 代碼的輸出 val is 10
pint is 4262128
*pint is 15
ref is 50
calling modify
in modify...
a is 0
p is 0
r is 0
returned from modify
val is 10
pint is 4262128
*pint is 7
ref is 0
這段代碼聲明了三個(gè)變量:兩個(gè)整型變量和一個(gè)指針變量。設(shè)置了每個(gè)變量的初始值并將其打印出來(lái)。同時(shí)打印出了指針值及其所指向的值。然后將所有三個(gè)變量作為參數(shù)傳遞給 modify 函數(shù)。前兩個(gè)參數(shù)是按值傳遞的,最后一個(gè)參數(shù)是按引用傳遞的。modify 函數(shù)的函數(shù)原型表明最后一個(gè)參數(shù)要作為引用傳遞;叵胍幌,C++ 按值傳遞所有參數(shù),引用除外,后者是按引用傳遞的。
modify 函數(shù)更改了所有三個(gè)參數(shù)的值:
將第一個(gè)參數(shù)設(shè)置為 0。
將第二個(gè)參數(shù)所指向的值設(shè)置為 7,然后將第二個(gè)參數(shù)設(shè)置為 0。
將第三個(gè)參數(shù)設(shè)置為 0。
將新值打印出來(lái),然后函數(shù)返回。當(dāng)執(zhí)行返回到 main 時(shí),再次打印出這三個(gè)參數(shù)的值以及指針?biāo)赶虻闹。作為第一個(gè)和第二個(gè)參數(shù)傳遞的變量不受 modify 函數(shù)的影響,因?yàn)樗鼈兪前粗祩鬟f的。但指針?biāo)赶虻闹蹈淖兞。?qǐng)注意,與前兩個(gè)參數(shù)不同,作為最后一個(gè)參數(shù)傳遞的變量被 modify 函數(shù)改變了,因?yàn)樗前匆脗鬟f的。
現(xiàn)在考慮用 Java 語(yǔ)言編寫的類似代碼:
清單 3:Java 應(yīng)用程序 class Test
{
public static void main(String args[])
{
int val;
StringBuffer sb1, sb2;
val = 10;
sb1 = new StringBuffer("apples");
sb2 = new StringBuffer("pears");
System.out.println("val is " + val);
System.out.println("sb1 is " + sb1);
System.out.println("sb2 is " + sb2);
System.out.println("");
System.out.println("calling modify");
//按值傳遞所有參數(shù)
modify(val, sb1, sb2);
System.out.println("returned from modify");
System.out.println("");
System.out.println("val is " + val);
System.out.println("sb1 is " + sb1);
System.out.println("sb2 is " + sb2);
}
public static void modify(int a, StringBuffer r1,
StringBuffer r2)
{
System.out.println("in modify...");
a = 0;
r1 = null;//1
r2.append(" taste good");
System.out.println("a is " + a);
System.out.println("r1 is " + r1);
System.out.println("r2 is " + r2);
}
}
這段代碼的輸出為:
清單 4:Java 應(yīng)用程序的輸出 val is 10
sb1 is apples
sb2 is pears
calling modify
in modify...
a is 0
r1 is null
r2 is pears taste good
returned from modify
val is 10
sb1 is apples
sb2 is pears taste good
這段代碼聲明了三個(gè)變量:一個(gè)整型變量和兩個(gè)對(duì)象引用。設(shè)置了每個(gè)變量的初始值并將它們打印出來(lái)。然后將所有三個(gè)變量作為參數(shù)傳遞給 modify 方法。
modify 方法更改了所有三個(gè)參數(shù)的值:
將第一個(gè)參數(shù)(整數(shù))設(shè)置為 0。
將第一個(gè)對(duì)象引用 r1 設(shè)置為 null。
保留第二個(gè)引用 r2 的值,但通過調(diào)用 append 方法更改它所引用的對(duì)象(這與前面的 C++ 示例中對(duì)指針 p 的處理類似)。
當(dāng)執(zhí)行返回到 main 時(shí),再次打印出這三個(gè)參數(shù)的值。正如預(yù)期的那樣,整型的 val 沒有改變。對(duì)象引用 sb1 也沒有改變。如果 sb1 是按引用傳遞的,正如許多人聲稱的那樣,它將為 null。但是,因?yàn)?Java 編程語(yǔ)言按值傳遞所有參數(shù),所以是將 sb1 的引用的一個(gè)副本傳遞給了 modify 方法。當(dāng) modify 方法在 //1 位置將 r1 設(shè)置為 null 時(shí),它只是對(duì) sb1 的引用的一個(gè)副本進(jìn)行了該操作,而不是像 C++ 中那樣對(duì)原始值進(jìn)行操作。
另外請(qǐng)注意,第二個(gè)對(duì)象引用 sb2 打印出的是在 modify 方法中設(shè)置的新字符串。即使 modify 中的變量 r2 只是引用 sb2 的一個(gè)副本,但它們指向同一個(gè)對(duì)象。因此,對(duì)復(fù)制的引用所調(diào)用的方法更改的是同一個(gè)對(duì)象。
編寫一個(gè)交換方法
假定我們知道參數(shù)是如何傳遞的,在 C++ 中編寫一個(gè)交換函數(shù)可以用不同的方式完成。使用指針的交換函數(shù)類似以下代碼,其中指針是按值傳遞的:
清單 5:使用指針的交換函數(shù) #include
#include
void swap(int *a, int *b);
int main (int argc, char** argv)
{
int val1, val2;
val1 = 10;
val2 = 50;
swap(&val1, &val2);
return 0;
}
void swap(int *a, int *b)
{
int temp = *b;
*b = *a;
*a = temp;
}
使用引用的交換函數(shù)類似以下代碼,其中引用是按引用傳遞的:
清單 6:使用引用的交換函數(shù) #include
#include
void swap(int &a, int &b);
int main (int argc, char** argv)
{
int val1, val2;
val1 = 10;
val2 = 50;
swap(val1, val2);
return 0;
}
void swap(int &a, int &b)
{
int temp = b;
b = a;
a = temp;
}
兩個(gè) C++ 代碼示例都像所希望的那樣交換了值。如果 Java 應(yīng)用程序使用“按引用傳遞”,則下面的交換方法應(yīng)像 C++ 示例一樣正常工作:
清單 7:Java 交換函數(shù)是否像 C++ 中那樣按引用傳遞參數(shù) class Swap
{
public static void main(String args[])
{
Integer a, b;
a = new Integer(10);
b = new Integer(50);
System.out.println("before swap...");
System.out.println("a is " + a);
System.out.println("b is " + b);
swap(a, b);
System.out.println("after swap...");
System.out.println("a is " + a);
System.out.println("b is " + b);
}
public static void swap(Integer a, Integer b)
{
Integer temp = a;
a = b;
b = temp;
}
}
因?yàn)?Java 應(yīng)用程序按值傳遞所有參數(shù),所以這段代碼不會(huì)正常工作,其生成的輸入如下所示:
清單 8:清單 7 的輸出 before swap...
a is 10
b is 50
after swap...
a is 10
b is 50
那么,在 Java 應(yīng)用程序中如何編寫一個(gè)方法來(lái)交換兩個(gè)基本類型的值或兩個(gè)對(duì)象引用的值呢?因?yàn)?Java 應(yīng)用程序按值傳遞所有的參數(shù),所以您不能這樣做。要交換值,您必須用在方法調(diào)用外部用內(nèi)聯(lián)來(lái)完成。
結(jié)論
我在書中包括該信息的意圖并不是作瑣細(xì)的分析或試圖使問題復(fù)雜化,而是想警告程序員:在 Java 應(yīng)用程序中假定“按引用傳遞”語(yǔ)義是危險(xiǎn)的。如果您在 Java 應(yīng)用程序中假定“按引用傳遞”語(yǔ)義,您就可能寫出類似上面的交換方法,然后疑惑它為什么不正常工作。
我必須承認(rèn),在我第一次認(rèn)識(shí)到 Java 應(yīng)用程序按值傳遞所有參數(shù)時(shí),我也曾表示懷疑。我曾一直假定因?yàn)?Java 應(yīng)用程序有兩種類型,所以他們按值傳遞基本類型而按引用傳遞引用,就像 C++ 那樣。在轉(zhuǎn)向 Java 編程之前我已用 C++ 編程好幾年了,感覺任何其他事情似乎都不直觀。但是,一旦我理解了發(fā)生的事情,我就相信 Java 語(yǔ)言按值傳遞所有參數(shù)的方法更加直觀。The Java Programming Language,Second Edition 的作者,Ken Arnold 和 James Gosling 在 2.6.1 節(jié)中說(shuō)得最好:“在 Java 中只有一種參數(shù)傳遞模式 -- 按值傳遞 -- 這有助于使事情保持簡(jiǎn)單。”