參數是按值而不是按引用傳遞的說明 Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞。寫它是爲了揭穿廣泛存在的一種神話,即認爲 Java 應用程序按引用傳遞參數,以免因依賴「按引用傳遞」這一行爲而致使的常見編程錯誤。
對此節選的某些反饋意見認爲,我把這一問題搞糊塗了,或者將它徹底搞錯了。許多不一樣意個人讀者用 C++ 語言做爲例子。所以,在此欄目中我將使用 C++ 和 Java 應用程序進一步闡明一些事實。
要點
讀完全部的評論之後,問題終於明白了,考試吧提示: 至少在一個主要問題上產生了混淆。由於對象是按引用傳遞的。對象確實是按引用傳遞的;節選與這沒有衝突。節選中說全部參數都是按值 -- 另外一個參數 -- 傳遞的。下面的說法是正確的:在 Java 應用程序中永遠不會傳遞對象,而只傳遞對象引用。所以是按引用傳遞對象。但重要的是要區分參數是如何傳遞的,這纔是該節選的意圖。Java 應用程序按引用傳遞對象這一事實並不意味着 Java 應用程序按引用傳遞參數。參數能夠是對象引用,而 Java 應用程序是按值傳遞對象引用的。
C++ 和 Java 應用程序中的參數傳遞
Java 應用程序中的變量能夠爲如下兩種類型之一:引用類型或基本類型。看成爲參數傳遞給一個方法時,處理這兩種類型的方式是相同的。兩種類型都是按值傳遞的;沒有一種按引用傳遞。這是一個重要特性,正如隨後的代碼示例所示的那樣。
在繼續討論以前,定義按值傳遞和按引用傳遞這兩個術語是重要的。按值傳遞意味着當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本。所以, 若是函數修改了該參數,僅改變副本,而原始值保持不變。按引用傳遞意味着當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本。 所以,若是函數修改了該參數,調用代碼中的原始值也隨之改變。
上面的這些是很重要的,請你們注意如下幾點結論,這些都是我認爲的上面的文章中的精華和最終的結論:
一、對象是按引用傳遞的
二、Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
三、按值傳遞意味着當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本
四、按引用傳遞意味着當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
首先考試吧來看看第一點:對象是按引用傳遞的
確實,這一點我想你們沒有任何疑問,例如:
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
下一個問題: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規則》)
一、對象是按引用傳遞的
二、Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
三、按值傳遞意味着當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本
四、按引用傳遞意味着當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
三句話總結一下:
1.對象就是傳引用c++
2.原始類型就是傳值程序員
3.String類型由於沒有提供自身修改的函數,每次操做都是新生成一個String對象,因此要特殊對待。能夠認爲是傳值。編程
==========================================================================app
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是傳引用,那麼傳遞的是引用的副本。)spa
在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都是這樣解釋的:「不論是基本類型仍是對象類型,都是傳值。」這種說法也不能算錯,由於它們把引用副本也當作是一種「值」。可是筆者認爲:傳值和 傳引用原本就是兩個不一樣的內容,不必把二者弄在一塊兒,弄在一塊兒反而更不易理解。