深入理解java的形參和實參


轉載聲明:本文轉載自公眾號「碼匠筆記」。

前幾天在頭條上看到一道經典面試題,引發了一些思考。也是寫這篇文章的導火索。

背景

請看題:

  1. public    classMain{

  2.    publicstaticvoid main(String[] args){

  3.        Integer a =1;

  4.        Integer b =2;

  5.        System.out.println("a="+ a +",b="+ b);

  6.        swap(a, b);

  7.        System.out.println("a="+ a +",b="+ b);

  8.    }

  9.  

  10.    privatestaticvoid swap(Integer numa,Integer numb){

  11.        //請實現

  12.    }

  13. }

看到這個題后 瞬間覺得有坑。也覺得為什么要書寫一個 swap方法呢?如下實現不是更簡單:

  1. publicstaticvoid main(String[] args){

  2.        Integer a =1;

  3.        Integer b =2;

  4.        System.out.println("a="+ a +",b="+ b);

  5.        Integer tmp = a;

  6.        a = b;

  7.        b = tmp;

  8.        System.out.println("a="+ a +",b="+ b);

  9.    }

輸出:

  1. a=1,b=2

  2. a=2,b=1

完美實現交換。但是請注意,這是一道面試題,要的就是考驗一些知識點。所以還是老老實實的實現 swap方法吧。 有的同學可能會想, Integer 是一個包裝類型,是對Int的裝箱和拆箱操作。其實也是一個對象。既然是對象,直接更改對象的引用不就行了?
思路沒問題,我們首先看看實現:

  1. privatestaticvoid swap(Integer numa,Integer numb){

  2.        Integer tmp = numa;

  3.        numa = numb;

  4.        numb = tmp;

  5.        System.out.println("numa="+ numa +",numb="+ numb);

  6. }

輸出:

  1. a=1,b=2

  2. numa=2,numb=1

  3. a=1,b=2

不出意外,沒有成功
這是什么原因呢? 技術老手一看就知道問題出在形參和實參混淆了

JAVA的形參和實參的區別:

形參 顧名思義:就是形式參數,用於定義方法的時候使用的參數,是用來接收調用者傳遞的參數的。 形參只有在方法被調用的時候,虛擬機才會分配內存單元,在方法調用結束之后便會釋放所分配的內存單元。 因此,形參只在方法內部有效,所以針對引用對象的改動也無法影響到方法外。

實參 顧名思義:就是實際參數,用於調用時傳遞給方法的參數。實參在傳遞給別的方法之前是要被預先賦值的。 在本例中 swap 方法 的numa, numb 就是形參,傳遞給 swap 方法的 a,b 就是實參

注意:
值傳遞調用過程中,只能把實參傳遞給形參,而不能把形參的值反向作用到實參上。在函數調用過程中,形參的值發生改變,而實參的值不會發生改變。
而在 引用傳遞調用的機制中,實際上是將實參引用的地址傳遞給了形參,所以任何發生在形參上的改變也會發生在實參變量上。
那么問題來了,什么是 值傳遞引用傳遞

值傳遞和引用傳遞

在談 值傳遞引用傳遞之前先了解下 Java的數據類型有哪些

JAVA的數據類型

Java 中的數據類型分為兩大類, 基本類型對象類型。相應的,變量也有兩種類型: 基本類型引用類型 基本類型的變量保存 原始值,即它代表的值就是數值本身, 原始值一般對應在內存上的 棧區
引用類型的變量保存 引用值引用值指向內存空間的地址。代表了某個對象的引用,而不是對象本身。對象本身存放在這個引用值所表示的地址的位置。 被引用的對象對應內存上的 堆內存區
基本類型包括: byte, short, int, long, char, float, double, boolean 這八大基本數據類型 引用類型包括: 類類型接口類型數組

變量的基本類型和引用類型的區別

基本數據類型在聲明時系統就給它分配空間

  1.    int a;

  2. //雖然沒有賦值,但聲明的時候虛擬機就會 分配 4字節 的內存區域,

  3. //而引用數據類型不同,它聲明時只給變量分配了引用空間,而不分配數據空間:    

  4.    String str;

  5. //聲明的時候沒有分配數據空間,只有 4byte 的引用大小,

  6. //在棧區,而在堆內存區域沒有任何分配

  7.    str.length();

  8. //這個操作就會報錯,因為堆內存上還沒有分配內存區域,而 a = 1; 這個操作就不會報錯。

好了,Java的數據類型說完了,繼續我們的 值傳遞引用傳遞的話題。 先背住一個概念: 基本類型的變量是 值傳遞引用類型的變量 結合前面說的 形參實參

值傳遞

方法調用時,實際參數把它的值傳遞給對應的形式參數,函數接收的是原始值的一個copy, 此時內存中存在兩個相等的基本類型,即實際參數和形式參數,后面方法中的操作都是對形參這個值的修改,不影響實際參數的值

引用傳遞

也稱為 地址傳遞址傳遞。方法調用時,實際參數的引用(地址,而不是參數的值)被傳遞給方法中相對應的形式參數,函數接收的是原始值的內存地址 在方法執行中,形參和實參內容相同,指向同一塊內存地址,方法執行中對引用的操作將會影響到實際對象 通過例子來說話:

  1. staticclassPerson{

  2.        int age;

  3.        Person(int age){

  4.            this.age = age;

  5.        }

  6.    }

  7.  

  8.    privatestaticvoid test(){

  9.        int a =100;

  10.        testValueT(a);

  11.        System.out.println("a="+ a);

  12.        Person person =newPerson(20);

  13.        testReference(person);

  14.        System.out.println("person.age="+ person.age);

  15.    }

  16.  

  17.    privatestaticvoid testValueT(int a){

  18.        a =200;

  19.        System.out.println("int testValueT a="+ a);

  20.    }

  21.  

  22.    privatestaticvoid testReference(Person person){

  23.        person.age =10;

  24.    }

輸出:

  1. int testValueT a=200

  2. a=100

  3. person.age=10

看見 值傳遞 a的值並沒有改變,而 引用傳遞的 persion.age已經改變了 有人說

  1. privatestaticvoid testReference(Person person){

  2.        person =newPerson(100);

  3. }

為什么 輸出的 person.age 還是20呢?
我想說 了解一下什么是 引用類型吧? 方法內把 形參的地址引用換成了另一個對象,並沒有改變這個對象,並不能影響 外邊 實參還引用原來的對象,因為 形參只在方法內有效哦。

有人或許還有疑問,按照文章開頭的例子, Integer也是 引用類型該當如何呢? 其實 類似的 String, Integer, Float, Double, Short, Byte, Long, Character等等基本包裝類型類。因為他們本身沒有提供方法去改變內部的值,例如 Integer內部有一個 value 來記錄 int基本類型的值,但是沒有提供修改它的方法,而且 也是 final類型的,無法通過 常規手段更改。
所以雖然他們是 引用類型的,但是我們可以認為它是 值傳遞,這個也只是 認為,事實上還是 引用傳遞, 址傳遞


好了,基礎知識補充完畢,然我們回到面試題吧


回歸正題

  1. privatestaticvoid swap(Integer numa,Integer numb){

  2.        Integer tmp = numa;

  3.        numa = numb;

  4.        numb = tmp;

  5.        System.out.println("numa="+ numa +",numb="+ numb);

  6. }

通過補習基礎知識,我們很明顯知道 上面這個方法實現替換 是不可行的。因為 Interger雖然是 引用類型
但是上述操作只是改變了 形參的引用,而沒有改變 實參對應的 對象

那么思路來了,我們 通過特殊手段改變 Integer內部的 value屬性

  1. privatestaticvoid swap(Integer numa,Integer numb){

  2.        Integer tmp = numa;

  3.        try{

  4.            Field field =Integer.class.getDeclaredField("value");

  5.            field.setAccessible(true);

  6.            field.set(numa, numb);//成功的將numa 引用的 1的對象 值改為 2

  7.            field.set(numb, tmp);//由於 tmp 也是指向 numa 未改變前指向的堆 即對象1 ,經過前一步,已經將對象1的值改為了2,自然 numb 也是2,所以改動失效

  8.        }catch(Exception e){

  9.            e.printStackTrace();

  10.        }

  11.    }

輸出結果:java a=1,b=2a=2,b=2又來疑問了?為何 a的值改變成功,而 b的改變失敗呢?

見代碼注釋 所以其實 field.set(numb,tmp); 是更改成功的,只是 tmp 經過前一行代碼的執行,已經變成了 2。 那么如何破呢? 我們有了一個思路,既然是 tmp的引用的對象值變量,那么我讓 tmp不引用 numa

  1. privatestaticvoid swap(Integer numa,Integer numb){

  2.        int tmp = numa.intValue();//tmp 定義為基本數據類型

  3.        try{

  4.            Field field =Integer.class.getDeclaredField("value");

  5.            field.setAccessible(true);

  6.            field.set(numa, numb);//這個時候並不改變 tmp 的值

  7.            field.set(numb, tmp);

  8.        }catch(Exception e){

  9.            e.printStackTrace();

  10.        }

  11.    }

這種情況下 對 numa 這個對象的修改就不會導致 tmp 的值變化了,看一下運行結果

  1. a=1,b=2

  2. a=2,b=2

這是為啥?有沒有 快瘋啦? 難道我們的思路錯了? 先別着急,我們看看這個例子: 僅僅是將前面的例子 a的值改為 129, b的值改為130

  1. publicstaticvoid main(String[] args){

  2.        Integer a =129;

  3.        Integer b =130;

  4.  

  5.        System.out.println("a="+ a +",b="+ b);

  6.        swap(a, b);

  7.        System.out.println("a="+ a +",b="+ b);

  8.    }

  9.  

  10.    privatestaticvoid swap(Integer numa,Integer numb){

  11.        int tmp = numa.intValue();

  12.        try{

  13.            Field field =Integer.class.getDeclaredField("value");

  14.            field.setAccessible(true);

  15.            field.set(numa, numb);

  16.            field.set(numb, tmp);

  17.        }catch(Exception e){

  18.            e.printStackTrace();

  19.        }

  20.    }

運行結果:

  1. a=129,b=130

  2. a=130,b=129

有沒有 懷疑人生?我們的思路沒有問題啊?為什么 換個數值就行了呢? 我們稍微修改一下程序

  1. publicstaticvoid main(String[] args){

  2.        Integer a =newInteger(1);

  3.        Integer b =newInteger(2);

  4.  

  5.        System.out.println("a="+ a +",b="+ b);

  6.        swap(a, b);

  7.        System.out.println("a="+ a +",b="+ b);

  8.    }

  9.  

  10.    privatestaticvoid swap(Integer numa,Integer numb){

  11.        int tmp = numa.intValue();

  12.        try{

  13.            Field field =Integer.class.getDeclaredField("value");

  14.            field.setAccessible(true);

  15.            field.set(numa, numb);

  16.            field.set(numb, tmp);

  17.        }catch(Exception e){

  18.            e.printStackTrace();

  19.        }

  20.    }

運行結果:

  1.     a=1,b=2

  2.     a=2,b=1

哎?為啥 1 和 2 也可以了?
我們這時肯定猜想和 Integer的裝箱 拆箱有關

裝箱,拆箱 概念

Integer的裝箱操作

為什么 Integera=1Integera=newInteger(1) 效果不一樣 那就瞅瞅源碼吧?

  1.    publicInteger(int value){

  2.        this.value = value;

  3.    }

  4.  

  5.    /**

  6.     * Returns an {@code Integer} instance representing the specified

  7.     * {@code int} value.  If a new {@code Integer} instance is not

  8.     * required, this method should generally be used in preference to

  9.     * the constructor {@link #Integer(int)}, as this method is likely

  10.     * to yield significantly better space and time performance by

  11.     * caching frequently requested values.

  12.     *

  13.     * This method will always cache values in the range -128 to 127,

  14.     * inclusive, and may cache other values outside of this range.

  15.     *

  16.     * @param  i an {@code int} value.

  17.     * @return an {@code Integer} instance representing {@code i}.

  18.     * @since  1.5

  19.     */

  20.    publicstaticInteger valueOf(int i){

  21.        if(i >=IntegerCache.low && i <=IntegerCache.high)

  22.            returnIntegerCache.cache[i +(-IntegerCache.low)];

  23.        returnnewInteger(i);

  24.    }  

通過注釋知道,java推薦 Integer.valueOf 方式初始化一個 Interger因為有 緩存了 -128-127的數字 我們直接定義 Integera=1 具有這個功能,所以 Jvm 底層實現 是通過 Integer.valueOf這個方法 再看 field.set(numb,tmp); 我們打斷點,發現通過反射設置 value時 竟然走了 Integer.valueOf 方法 下面是 我們調用 swap前后的 IntegerCache.cache 值得變化

反射修改前:

反射修改后

在反射修改前

  1. IntegerCache.cache[128]=0

  2. IntegerCache.cache[129]=1

  3. IntegerCache.cache[130]=2

通過反射修改后

  1. IntegerCache.cache[128]=0

  2. IntegerCache.cache[129]=2

  3. IntegerCache.cache[130]=2

再調用 field.set(numb,tmp) tmp這時等於1 對應的 角標 129 ,但是這個值已經變成了2 所以出現了剛才 奇怪的結果原來都是 緩存的鍋下面趁機再看個例子 加深理解

  1. Integer testA =1;

  2. Integer testB =1;

  3.  

  4. Integer testC =128;

  5. Integer testD =128;

  6. System.out.println("testA=testB "+(testA == testB)+",\ntestC=testD "+(testC == testD));    

輸出結果:

java testA=testBtrue,testC=testDfalse通過這小示例,在 -128 到 127的數字都走了緩存,這樣 testAtestB引用的是同一片內存區域的同一個對象。 而 testC testD 數值大於127 所以 沒有走緩存,相當於兩個 Integer對象,在堆內存區域有兩個對象。 兩個對象自如不相等。
在前面的示例中 我們 通過

  1. Integer a =newInteger(1);

  2. Integer b =newInteger(2);

方式初始化 a, b 我們的交換算法沒有問題,也是這個原因。

那么到目前為止我們的 swap 方法可以完善啦
  1. privatestaticvoid swap(Integer numa,Integer numb){

  2.        int tmp = numa.intValue();

  3.        try{

  4.            Field field =Integer.class.getDeclaredField("value");

  5.            field.setAccessible(true);

  6.            field.set(numa, numb);

  7.            field.set(numb,newInteger(tmp));

  8.        }catch(Exception e){

  9.            e.printStackTrace();

  10.        }

  11.    }

只需將之前的 field.set(numb,tmp) 改為 field.set(numb,newInteger(tmp))

到此, 這個面試我們已經通過了,還有一個疑問我沒有解答。 為什么 field.set(numb,tmp) 會執行 Integer.valueOf()field.set(numb,newInteger(tmp)) 不會執行。 這就是 Integer的裝箱操作,當 給 Integer.value 賦值 int時,JVM 檢測到 int不是Integer類型,需要裝箱,才執行了 Integer.valueOf()方法。而 field.set(numb,newInteger(tmp)) 設置的 是Integer類型了,就不會再拆箱后再裝箱。

Over Thanks


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM