Java傳參方式-值傳遞還是引用傳遞
參數是按值而不是按引用傳遞的說明 Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞。寫它是為了揭穿普遍存在的一種神話,即認為 Java 應用程序按引用傳遞參數,以避免因依賴“按引用傳遞”這一行為而導致的常見編程錯誤。
對此節選的某些反饋意見認為,我把這一問題搞糊涂了,或者將它完全搞錯了。許多不同意我的讀者用 C++ 語言作為例子。因此,在此欄目中我將使用 C++ 和 Java 應用程序進一步闡明一些事實。
要點
讀完所有的評論以后,問題終于明白了,考試吧提示: 至少在一個主要問題上產生了混淆。因為對象是按引用傳遞的。對象確實是按引用傳遞的;節選與這沒有沖突。節選中說所有參數都是按值 -- 另一個參數 -- 傳遞的。下面的說法是正確的:在 Java 應用程序中永遠不會傳遞對象,而只傳遞對象引用。因此是按引用傳遞對象。但重要的是要區分參數是如何傳遞的,這才是該節選的意圖。Java 應用程序按引用傳遞對象這一事實并不意味著 Java 應用程序按引用傳遞參數。參數可以是對象引用,而 Java 應用程序是按值傳遞對象引用的。
C++ 和 Java 應用程序中的參數傳遞
Java 應用程序中的變量可以為以下兩種類型之一:引用類型或基本類型。當作為參數傳遞給一個方法時,處理這兩種類型的方式是相同的。兩種類型都是按值傳遞的;沒有一種按引用傳遞。這是一個重要特性,正如隨后的代碼示例所示的那樣。
在繼續討論之前,定義按值傳遞和按引用傳遞這兩個術語是重要的。按值傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本。因此,如果函數修改了該參數,僅改變副本,而原始值保持不變。按引用傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本。因此,如果函數修改了該參數,調用代碼中的原始值也隨之改變。
上面的這些是很重要的,請大家注意以下幾點結論,這些都是我認為的上面的文章中的精華和最終的結論:
1、對象是按引用傳遞的
2、Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
3、按值傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本
4、按引用傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
首先考試吧來看看第一點:對象是按引用傳遞的
確實,這一點我想大家沒有任何疑問,例如:
class Test01
{
public static void main(String[] args)
{
StringBuffer s= new StringBuffer("good");
StringBuffer s2=s;
s2.append(" afternoon.");
System.out.println(s);
}
}
對象s和s2指向的是內存中的同一個地址因此指向的也是同一個對象。
如何解釋“對象是按引用傳遞的”的呢?
這里的意思是進行對象賦值操作是傳遞的是對象的引用,因此對象是按引用傳遞的,有問題嗎?
程序運行的輸出是:
good afternoon.
這說明s2和s是同一個對象。
這里有一點要澄清的是,這里的傳對象其實也是傳值,因為對象就是一個指針,這個賦值是指針之間的賦值,因此在java中就將它說成了傳引用。(引用是什么?不就是地址嗎?地址是什么,不過就是一個整數值)
再看看下面的例子:
class Test02
{
public static void main(String[] args)
{
int i=5;
int i2=i;
i2=6;
System.out.println(i);
}
}
程序的結果是什么?5!!!
這說明什么,原始數據類型是按值傳遞的,這個按值傳遞也是指的是進行賦值時的行為。
下一個問題:Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
class Test03 { public static void main(String[] args) { StringBuffer s= new StringBuffer("good"); StringBuffer s2=new StringBuffer("bad"); test(s,s2); System.out.println(s);//9 System.out.println(s2);//10 } static void test(StringBuffer s,StringBuffer s2) { System.out.println(s);//1 System.out.println(s2);//2 s2=s;//3 s=new StringBuffer("new");//4 System.out.println(s);//5 System.out.println(s2);//6 s.append("hah");//7 s2.append("hah");//8 } } |
程序的輸出是:
good
bad
new
good
goodhah
bad
考試吧提示: 為什么輸出是這樣的?
這里需要強調的是“參數傳遞機制”,它是與賦值語句時的傳遞機制的不同。
我們看到1,2處的輸出與我們的預計是完全匹配的
3將s2指向s,4將s指向一個新的對象
因此5的輸出打印的是新創建的對象的內容,而6打印的原來的s的內容
7和8兩個地方修改對象內容,但是9和10的輸出為什么是那樣的呢?
Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞。
至此,我想總結一下我對這個問題的最后的看法和我認為可以幫助大家理解的一種方法:
我們可以將java中的對象理解為c/c++中的指針
例如在c/c++中:
int *p;
print(p);//1
*p=5;
print(*p);//2
1打印的結果是什么,一個16進制的地址,2打印的結果是什么?5,也就是指針指向的內容。
即使在c/c++中,這個指針其實也是一個32位的整數,我們可以理解我一個long型的值。
而在java中一個對象s是什么,同樣也是一個指針,也是一個int型的整數(對于JVM而言),我們在直接使用(即s2=s這樣的情況,但是對于System.out.print(s)這種情況例外,因為它實際上被晃猄ystem.out.print(s.toString()))對象時它是一個int的整數,這個可以同時解釋賦值的傳引用和傳參數時的傳值(在這兩種情況下都是直接使用),而我們在s.XXX這樣的情況下時s其實就是c/c++中的*s這樣的使用了。這種在不同的使用情況下出現不同的結果是java為我們做的一種簡化,但是對于c/c++程序員可能是一種誤導。java中有很多中這種根據上下文進行自動識別和處理的情況,下面是一個有點極端的情況:
class t { public static String t="t"; public static void main(String[] args) { t t =new t(); t.t(); } static void t() { System.out.println(t); } } |
(關于根據上下文自動識別的內容,有興趣的人以后可以看看我們翻譯的《java規則》)
1、對象是按引用傳遞的
2、Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
3、按值傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本
4、按引用傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
三句話總結一下:
1.對象就是傳引用
2.原始類型就是傳值
3.String類型因為沒有提供自身修改的函數,每次操作都是新生成一個String對象,所以要特殊對待。可以認為是傳值。
==========================================================================
public class Test03 { public static void stringUpd(String str) { str = str.replace("j", "l"); System.out.println(str); } public static void stringBufferUpd(StringBuffer bf) { bf.append("c"); System.out.println(bf); } public static void main(String[] args) { /** * 對於基本類型和字符串(特殊)是傳值 * * 輸出lava,java */ String s1 = new String("java"); stringUpd(s1); System.out.println(s1); /** * 對於對象而言,傳的是引用,而引用指向的是同一個對象 * * 輸出javac,javac */ StringBuffer bb = new StringBuffer("java"); stringBufferUpd(bb); System.out.println(bb); } } |
解析:就像光到底是波還是粒子的問題一樣眾說紛紜,對于Java參數是傳值還是傳引用的問題,也有很多錯誤的理解和認識。我們首先要搞清楚一點就是:不管Java參數的類型是什么,一律傳遞參數的副本。對此,thinking in Java一書給出的經典解釋是When you’re passing primitives into a method, you get a distinct copy of the primitive. When you’re passing a reference into a method, you get a copy of the reference.(如果Java是傳值,那么傳遞的是值的副本;如果Java是傳引用,那么傳遞的是引用的副本。)
在Java中,變量分為以下兩類:
① 對于基本類型變量(int、long、double、float、byte、boolean、char),Java是傳值的副本。(這里Java和C++相同)
② 對于一切對象型變量,Java都是傳引用的副本。其實傳引用副本的實質就是復制指向地址的指針,只不過Java不像C++中有顯著的*和&符號。(這里Java和C++不同,在C++中,當參數是引用類型時,傳遞的是真實引用而不是引用副本)
需要注意的是:String類型也是對象型變量,所以它必然是傳引用副本。不要因為String在Java里面非常易于使用,而且不需要new,就被蒙蔽而把String當做基本變量類型。只不過String是一個非可變類,使得其傳值還是傳引用顯得沒什么區別。
對基本類型而言,傳值就是把自己復制一份傳遞,即使自己的副本變了,自己也不變。而對于對象類型而言,它傳的引用副本(類似于C++中的指針)指向自己的地址,而不是自己實際值的副本。為什么要這么做呢?因為對象類型是放在堆里的,一方面,速度相對于基本類型比較慢,另一方面,對象類型本身比較大,如果采用重新復制對象值的辦法,浪費內存且速度又慢。就像你要張三(張三相當于函數)打開倉庫并檢查庫里面的貨物(倉庫相當于地址),有必要新建一座倉庫(并放入相同貨物)給張三么? 沒有必要,你只需要把鑰匙(引用)復制一把寄給張三就可以了,張三會拿備用鑰匙(引用副本,但是有時效性,函數結束,鑰匙銷毀)打開倉庫。
在這里提一下,很多經典書籍包括thinking in Java都是這樣解釋的:“不管是基本類型還是對象類型,都是傳值。”這種說法也不能算錯,因為它們把引用副本也當做是一種“值”。但是筆者認為:傳值和傳引用本來就是兩個不同的內容,沒必要把兩者弄在一起,弄在一起反而更不易理解。