咱們先回顧一下基本概念java
參數在編程語言中是執行程序須要的數據,這個數據通常保存在變量中。在Java中定義一個方法時,能夠定義一些參數,
舉個例子:編程
public class Example { public static void main(String[] args) { String myName = "hawk"; sayYourName(myName);// 實際參數是myName } public static void sayYourName(String name) {// 形式參數是name System.out.println(name); } }
上面的代碼中定義一個名爲sayYourName的方法,若是想要執行這個方法,那麼你須要傳入一個String類型的變量給這個方法,定義這個方法時聲明的String類型的name就是形式參數,而在這個方法執行時傳入的myName就是實際參數。編程語言
實際參數是調用有參方法的時候真正傳遞的內容,而形式參數是用於接收實參內容的參數。函數
按值傳遞(call by value)lua
按值傳遞,就是指在調用函數時,將實參對應的值作一個拷貝指向函數對應的形參。在函數內改變形參對應的值並不會影響外部實參的值設計
按引用傳遞 (call by reference)code
按引用傳遞,是指在調用函數時,傳遞給函數的是實參的地址即引用,而不是實參的拷貝。在函數內部參數的值,對外部的實參是可見的。對象
按共享傳遞 (call by sharing)內存
按共享傳遞,是指在調用函數時,傳遞給函數的是實參的地址的拷貝(若是實參在棧中,則直接拷貝該值)。在函數內部對參數進行操做時,須要先拷貝的地址尋找到具體的值,再進行操做。若是該值在棧中,那麼由於是直接拷貝的值,因此函數內部對參數進行操做不會對外部變量產生影響。若是原來拷貝的是原值在堆中的地址,那麼須要先根據該地址找到堆中對應的位置,再進行操做。由於傳遞的是地址的拷貝因此函數內對值的操做對外部變量是可見的。get
按共享傳遞能夠理解爲按值傳遞的一個特例,這裏的值是對象的引用地址,而不是具體對象。
上面描述的傳參方式其實是函數調用時參數的求值策略(Evaluation Strategy),這個其實比較好理解,好比咱們調用上面的sayYourName()方法
咱們能夠這樣:sayYourName("hawk");
能夠這樣:sayYourName("ha"+"wk");
能夠這樣:sayYourName("hawk"+x.subString(2));// 此處的x是一個變量,進行截取後和"hawk"合併
可是全部這些實參的形式,都統稱爲表達式(Expression)。求值(Evaluation)便是指對這些表達式的簡化並求解其值的過程。
求值策略(值傳遞和引用傳遞)的關注的點在於,這些表達式在調用函數的過程當中,求值的時機、值的形式的選取等問題。求值的時機,能夠是在函數調用前,也能夠是在函數調用後,由被調用者本身求值。這裏所謂調用後求值,能夠理解爲Lazy Load或On Demand的一種求值方式。
並且,除了按值傳遞和按引用傳遞,還有一些其它的求值策略。這些求值策略的劃分依據是:求值的時機(調用前仍是調用中)和值自己的傳遞方式。詳見下表:
求值策略 | 求值時間 | 傳值方式 |
按值傳遞( pass by value ) | 調用前 | 值的結果(只是結果的副本) |
按引用傳遞( pass by reference ) | 調用前 | 原值(原始對象,無副本) |
按名傳遞( pass by name ) | 調用後(用到才求值) | 與值無關的一個名 |
按值傳遞 | 按引用傳遞 | |
本質區別 | 建立副本 | 不建立副本 |
表現結果 | 函數中沒法改變原始對象 | 函數中能夠改變原始對象 |
只要是按值傳遞,無論傳遞的參數類型是值類型仍是引用類型,都會在調用棧上面建立一個實參的副本,區別只是若是傳遞的參數類型是值類型,該副本就是實際參數的值的複製,而對於引用類型來講,引用類型的實例(即對象)是保存在堆中的,在棧上只有一個該實例的引用(通常狀況下是該實例在堆中的內存地址),此時,實際參數的副本並非該對象,而是引用的副本。
因此,綜上所述,對於Java的函數調用方式最準確的描述是:參數藉由值傳遞方式,傳遞的值是個引用。(句中兩個「值」不是一個意思,第一個值是evaluation result,第二個值是value content)
說到Java是值傳遞仍是引用傳遞,通常都會舉下面三個例子,咱們來一一說明一下:
第一個例子,基本類型參數傳值:
public class Example { public static void main(String[] args) { int i = 10; changeValue(i); System.out.println(i); } public static void changeValue(int a ) { a = 20; } }
上面的代碼解釋以下:
定義了一個int類型的變量i,並賦值爲10
調用changeValue方法,傳入i,changeValue方法將傳入的變量賦值爲20
輸出i
結果是:10
changValue方法內部對形參a的修改並無影響到實參i
第一個結論:Java中基本類型的傳值方式是按值傳遞
第二個例子,引用類型傳值:
public class Example { public static void main(String[] args) { String i = "hawk"; changString(i); System.out.println(i); } public static void changString(String a ) { a = "HAWK"; } }
上面的代碼解釋以下:
定義一個String類型的變量i,賦值爲"hawk"
調用changeString方法,將傳入的變量賦值爲"HAWK"
輸出i
結果: hawk
在Java中String類型雖然能夠直接賦值,可是是引用類型,由於String類型的具體值保存在堆中,而不是棧上
既然String是引用類型, 上面的例子又說明String是值傳遞的 那麼咱們是否是就能夠得出:
Java中引用類型也是值傳遞的,這樣的結論呢?
咱們來看第三個例子:
public class Example { int i = 20; public static void main(String[] args) { Example ex = new Example(); changValue(ex); System.out.println(ex.i); } public static void changValue(Example e ) { e.i = 30; } }
上面的代碼解釋以下:
實例化一個Example對象ex,ex中只有一個屬性i,初始值爲20
調用changValue方法,將ex中的屬性i的值賦值爲30
輸出ex中屬性i的值
結果是:30
這樣的結果就讓不少人產生了困惑,String類型是引用類型,對象也是引用類型,爲何第二個例子不能改變i的值,第三個例子卻改變了ex中i的值呢?Java中引用類型的參數傳遞究竟是引用傳遞仍是值傳遞呢?
其實,第二個例子和第三個例子咱們所關注的點已是錯誤的了,天然沒法得出正確的結論
咱們回顧一下引用傳遞和值傳遞的本質區別和行爲表現上的區別
按值傳遞 | 按引用傳遞 | |
本質區別 | 建立副本 | 不建立副本 |
表現結果 | 函數中沒法改變原始對象 | 函數中能夠改變原始對象 |
重要依據:若是能夠改變對象的引用,就說明是引用傳遞,若是不能改變對象的引用就說明是值傳遞
下面咱們多舉一個例子:
public class Example { int i = 20; public static void main(String[] args) { Example ex = new Example(); changValue(ex); System.out.println(ex.i); } public static void changValue(Example e ) { e = new Example(); e.i = 50; } }
這個例子和上面第三個例子很類似,區別在於:
changValue方法中將一個新的原始對象的引用賦值給了形參e
若是Java是按引用傳遞的話,e = new Example();就是修改了實參ex的引用,即就是改變了原始對象
若是Java是按值傳遞的話,實參ex不會有任何變化
結果:20
這個結果說明:changValue方法並無修改到ex的引用,也就是說,e只是ex的副本,對e的引用進行的全部操做都不會影響到ex,因此咱們
從上述代碼運行的結果也能夠證實Java中是隻有值傳遞的
爲何說Java中只有值傳遞
按值傳遞、按引用傳遞、按共享傳遞
爲何 Java 只有值傳遞,但 C# 既有值傳遞,又有引用傳遞,這種語言設計有哪些好處?