1. 程式人生 > >Java函式的傳參機制——你真的瞭解嗎?

Java函式的傳參機制——你真的瞭解嗎?

  這篇部落格的靈感來源於我參加的一次面試,面試官與我比較深入的討論了Java函式傳參的問題,當時與他討論了好半天,不知道是面試官自己沒徹底弄清楚還是因為他故意想深入考察我對這個知識點的瞭解程度。Java函式的傳參機制是一個非常基礎的知識點,說難不難,但是想要徹底弄清楚也沒那麼容易,剛到公司時,發現部分同事對這個機制瞭解得不是很清楚,於是給他們仔細地講了講,他們聽了之後有些恍然大悟,所以我猜測可能會有一些有經驗的Java工程師對此知識點的理解也會比較模糊。鑑於此,作為我的第一篇技術型的博文,在此把自己的理解寫出來,歡迎大家批評指正。
  作為一名Java工程師,我對C++也有一些瞭解,個人覺得Java與C++有著很多的共同之處,所以在討論Java的函式傳參機制時,我想先以一道比較經典的C++試題作為引子。

題目一:請用C++實現swap函式,交換兩個整數型別的值。

  這是一道C++初學者經常會碰到的一個問題,先看下面的這種實現方式

int main(int argc, char *argv){
    int a = 1, b=2;
    swap(a,b);
}
void swap(int a, int b){
    int temp = a;
    a = b;
    b = temp
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  很顯然,只要稍有點C++基礎的程式設計師都會知道上述實現方式達不到目的,因為在函式引數的傳遞過程中,形參只是將實參的值拷貝了一份,在接下來的函式體中,所有的賦值操作都是針對於形參的,而對實參的值不會有任何的改變,也就是說在呼叫了swap函式之後,a的值仍為1,b的值仍為2,並未達到交換他們的值的目的。
  接下來另一種實現方式

int main(int argc, char *argv){
    int a = 1, b=2;
    swap(&a,&b);
}
void swap(int* a, int* b){
    int temp = *a;
    *a = *b;
    *b = temp;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  這一次的實現是對的,設計的函式的形參為兩個指標變數,然後將兩個變數的指標作為引數傳入,在函式實現體的內部,將兩個指標的指向的變數進行賦值,改變了引用a和b指向的地址的值,從而達到了交換a、b的值的目的。
  以上面這道簡單的C++的試題作為背景,我們將其延伸到Java語言上來,來一道新的題目:

題目二:請用Java實現swap函式,交換兩個整數型別的值。

  實現方式一:

public static void main(String[] args){
    int a = 1, b=2;
    swap(a,b);
    System.out.print("a="+ a + ", b="+ b);
}
void swap(int a, int b){
    int temp = a;
    a = b;
    b = temp;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  很顯然,這種實現方式不對,錯誤原因與上面C++的第一種實現方式類似,就不再贅述。既然這種方式不對,那麼我們能否借鑑C++的第二種實現方式呢?想必很多Java學習者都聽過這樣一些論斷:“Java裡面的引用與C++裡面的指標類似”、“Java裡面處處是指標”等。暫且不管這些論斷是否正確,我們借鑑這樣一種思想嘗試著實現上面的swap函式:既然用基本型別的變數實現swap函式,那麼我們改用包裝型別,由於包裝型別傳入的是引用,而引用與C++的指標類似,因此有可能實現swap函式。下面嘗試第二種方式:

public static void main(String[] args){
    Integer a = 1, b = 2;
    swap(a,b);
    System.out.print("a="+ a + ", b="+ b);
}
void swap(Integer a, Integer b){
    Integer temp = a;
    a = b;
    b = temp;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  請仔細思考上面的程式碼,我們通過呼叫swap函式是否實現了a,b兩個整數的交換?
  執行程式碼之後,我們發現a的值仍舊為1,b的值仍舊為2,兩個變數的值依然沒有被交換。這種方式居然也行不通,那原因是啥呢?是由於Java裡的引用跟C++的裡面的指標有區別,還是由於Java與C++的函式傳參機制不同呢?先不忙著解答這個問題,待我稍後揭開其中的原因。
  既然第二種方式使用的是不可變的整型包裝類Integer達不到目的,那麼我們使用嘗試著使用非final類AtomicInteger,下面我們看程式碼(實現方式三):

public static void main(String[] args){  
    AtomicInteger a = 1, b = 2;
    swap(a,b);
    System.out.print("a="+ a + ", b="+ b);
}
void swap(Integer a, Integer b){  
    AtomicInteger temp = a;
    a = b;
    b = temp;
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  執行程式碼之後我們很遺憾地發現,這一次我們仍舊沒有成功地交換a、b兩個整數的值。最後請看下面幾行程式碼(實現方式四):

public static void main(String[] args){  
    AtomicInteger a = 1, b = 2;
    swap(a,b);
    System.out.print("a="+ a + ", b="+ b);
}
void swap(Integer a, Integer b){  
    int temp = a.get();
    a.set(b.get());
    b.set(temp);
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  這一次我們神奇地發現,a、b兩個整數發生了交換!真是不容易呀,折騰半天終於用Java語言實現了兩個整數的交換。看到這裡可能有人已經按捺不住了,說我扯了這麼多也就是用Java實現了一個swap函式而已,也沒有講明白Java函式的傳參機制啊。稍安勿躁,下面請聽我細細道來其中原委。
  首先,我想講一下Java語言中變數的儲存機制。在Java中,我們宣告並初始化一個變數的時候,會產生一個基本型別或者物件的引用以及其這個基本型別或者物件本身,而我們程式碼中看到的變數就是這個引用,無論何時我們想呼叫這個基本型別或者物件的時候我們只能通過其引用來間接呼叫。這裡其實就告訴了大家Java裡的引用與C++裡面的指標的不通之處。對於C++的指標,我們可以通過對一個指標變數進行*操作,然後進行賦值(也就是上面的 *a = *b;*b = temp這兩行程式碼),來改變這個指標變數指向的地址的真實資料,而Java裡面的引用則不同,它沒有*操作,對Java引用的賦值操作只會將這個引用指向一個新的物件,原來的物件不變。因此,改變Java物件的唯一方法(反序列化等非常規方法除外)就是通過引用呼叫這個物件的能夠改變自身屬性的方法(比如AtomicInteger的set方法)。
  在上面基本事實作為前提條件下,我們來一個個來分析上面四段Java程式碼,來解讀Java函式的傳參機制。對於實現方式一,基本型別作為函式引數進行傳遞,傳遞的是值(其實我更傾向於另外一種說法,下文中會提到),也就是把實參的值拷貝一份傳給形參,之後在函式體中所有的操作都是在形參上,永遠不會改變實參的值。關於實現方式二,通過物件進行傳遞,其實傳遞的是引用,也就是將物件的引用拷貝一份傳給形參,形參引用和實參引用指向的是同一個物件,但是在函式體中,我們所有的操作都只是對形參引用進行賦值運算,而對應用的賦值運算只會讓引用重新指向另一個物件,因此函式體只是將這些拷貝出來的引用重新指向其它物件,並沒有改變這些物件的值,也沒有改變原來的實參引用指向的物件,因此該函式並沒有達到交換兩個整數的目的。實現方式三,其原理跟實現方式三完全一致,唯一的區別就是將Integer物件換成了AtomicInteger物件,其根本思路沒變,所以也沒有達到交換的目的。對於實現方式四,由於函式引數也是物件,按照前面的說法,傳遞的也是引用,因此形參引用a與實參引用a指向的是同一個物件,形參引用b與實參引用b指向的也是同一個物件,而在函式體中,分別通過呼叫了形參a和形參b的set方法來改變了物件本身,因此也同時改變了實參引用a和實參引用b指向的物件,達到了交換a、b兩個整數的目的。一言以蔽之,Java函式傳遞時,基本型別傳遞的是值,物件型別傳遞的是引用,無論是基本型別還是物件型別,在函式體中沒有改變物件的操作的話原來物件就不會改變!
  對於上面的解釋和總結,可能有人會問:你說物件傳遞的是引用,那要是我們的實參是一個匿名物件,沒有引用,那怎麼把引用傳遞給形參呢?比如將第四段Java程式碼改為如下方式:

public static void main(String[] args){
    AtomicInteger a = 1;  
    swap(a, new AtomicInteger(2));  
    System.out.print("a="+ a);  
}  
void swap(Integer a, Integer b){  
    int temp = a.get();  
    a.set(b.get());  
    b.set(temp);  
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  其中的new AtomicInteger(2)是怎麼將引用傳遞給b的呢?實際上Java中所有的東西在JVM內部都是編號,名字只是給人看的,匿名物件沒有名字但有編號,傳遞引數時外面是沒給它取名字,但被呼叫的方法中是有個形引數和它對應起來的,這樣在被呼叫的方法內部它還是有名字的。另外,所以的匿名的東西,不管是匿名類還是匿名物件,在編譯的那一刻就已經給了一個臨時的名字,比如 1,” role=”presentation” style=”position: relative;”>1,1,2 這樣的名字,所以匿名的物件也是通過引用傳遞給形參的,在編譯的時候會給它指定一個引用。
  還記得我在上面解釋第一段程式碼的時候加了一個括號的說明嗎?我更加傾向的是那種說法是啥呢?如果有興趣的話請先閱讀王垠的這篇文章——《Java有值型別嗎?》。假設各位已經接受文Java沒有值型別的觀點,Java基本型別也是一種引用型別,那麼Java函式的引數傳遞可以用更加簡單的一句話來概括——Java函式引數傳遞的是引用!至於為什麼,請大家結合王垠的那篇文章以及我前面的分析進行思考。
  最後,出個小題,測試一下大家是否真正的理解了前面講的知識點。答案會在以後給出。

題目三:在下面的程式碼塊中func函式執行完畢後,主函式中的list1和list2中分別有哪些元素呢?(請用JDK1.7以上的版本執行)
public static void main(String[] args){
    List<Integer> list1 = new ArrayList<>();
    List<Integer> list2 = new ArrayList<>();
    list1.add(0);
    list2.add(0);
    func(list1,list2);
}
void func(List<Integer> list1,List<Integer> list2){
    list1 = new ArrayList<>();
    list1.add(1);
    list2.add(1);
    list2 = new ArrayList<>();
    list1.add(2);
    list2.add(2);
}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

轉自:https://blog.csdn.net/whuxinxie/article/details/54895768