轉載聲明:本文轉載自公眾號「碼匠筆記」。
前幾天在頭條上看到一道經典面試題,引發了一些思考。也是寫這篇文章的導火索。
背景
請看題:
-
public classMain{
-
publicstaticvoid main(String[] args){
-
Integer a =1;
-
Integer b =2;
-
System.out.println("a="+ a +",b="+ b);
-
swap(a, b);
-
System.out.println("a="+ a +",b="+ b);
-
}
-
-
privatestaticvoid swap(Integer numa,Integer numb){
-
//請實現
-
}
-
}
看到這個題后 瞬間覺得有坑。也覺得為什么要書寫一個 swap
方法呢?如下實現不是更簡單:
-
publicstaticvoid main(String[] args){
-
Integer a =1;
-
Integer b =2;
-
System.out.println("a="+ a +",b="+ b);
-
Integer tmp = a;
-
a = b;
-
b = tmp;
-
System.out.println("a="+ a +",b="+ b);
-
}
輸出:
-
a=1,b=2
-
a=2,b=1
完美實現交換。但是請注意,這是一道面試題,要的就是考驗一些知識點。所以還是老老實實的實現 swap
方法吧。 有的同學可能會想, Integer
是一個包裝類型,是對Int的裝箱和拆箱操作。其實也是一個對象。既然是對象,直接更改對象的引用不就行了?
思路沒問題,我們首先看看實現:
-
privatestaticvoid swap(Integer numa,Integer numb){
-
Integer tmp = numa;
-
numa = numb;
-
numb = tmp;
-
System.out.println("numa="+ numa +",numb="+ numb);
-
}
輸出:
-
a=1,b=2
-
numa=2,numb=1
-
a=1,b=2
不出意外,沒有成功
這是什么原因呢? 技術老手一看就知道問題出在形參和實參混淆了
JAVA的形參和實參的區別:
形參 顧名思義:就是形式參數,用於定義方法的時候使用的參數,是用來接收調用者傳遞的參數的。 形參只有在方法被調用的時候,虛擬機才會分配內存單元,在方法調用結束之后便會釋放所分配的內存單元。 因此,形參只在方法內部有效,所以針對引用對象的改動也無法影響到方法外。
實參 顧名思義:就是實際參數,用於調用時傳遞給方法的參數。實參在傳遞給別的方法之前是要被預先賦值的。 在本例中 swap 方法 的numa, numb 就是形參,傳遞給 swap 方法的 a,b 就是實參
注意:
在 值傳遞
調用過程中,只能把實參傳遞給形參,而不能把形參的值反向作用到實參上。在函數調用過程中,形參的值發生改變,而實參的值不會發生改變。
而在 引用傳遞
調用的機制中,實際上是將實參引用的地址傳遞給了形參,所以任何發生在形參上的改變也會發生在實參變量上。
那么問題來了,什么是 值傳遞
和 引用傳遞
值傳遞和引用傳遞
在談 值傳遞
和 引用傳遞
之前先了解下 Java的數據類型有哪些
JAVA的數據類型
Java 中的數據類型分為兩大類, 基本類型
和 對象類型
。相應的,變量也有兩種類型: 基本類型
和 引用類型
基本類型
的變量保存 原始值
,即它代表的值就是數值本身, 原始值
一般對應在內存上的 棧區
而 引用類型
的變量保存 引用值
, 引用值
指向內存空間的地址。代表了某個對象的引用,而不是對象本身。對象本身存放在這個引用值所表示的地址的位置。 被引用的對象
對應內存上的 堆內存區
。
基本類型包括: byte
, short
, int
, long
, char
, float
, double
, boolean
這八大基本數據類型 引用類型包括: 類類型
, 接口類型
和 數組
變量的基本類型和引用類型的區別
基本數據類型在聲明時系統就給它分配空間
-
int a;
-
//雖然沒有賦值,但聲明的時候虛擬機就會 分配 4字節 的內存區域,
-
//而引用數據類型不同,它聲明時只給變量分配了引用空間,而不分配數據空間:
-
String str;
-
//聲明的時候沒有分配數據空間,只有 4byte 的引用大小,
-
//在棧區,而在堆內存區域沒有任何分配
-
str.length();
-
//這個操作就會報錯,因為堆內存上還沒有分配內存區域,而 a = 1; 這個操作就不會報錯。
好了,Java的數據類型說完了,繼續我們的 值傳遞
和 引用傳遞
的話題。 先背住一個概念: 基本類型
的變量是 值傳遞
; 引用類型
的變量 結合前面說的 形參
和 實參
。
值傳遞
方法調用時,實際參數把它的值傳遞給對應的形式參數,函數接收的是原始值的一個copy, 此時內存中存在兩個相等的基本類型,即實際參數和形式參數,后面方法中的操作都是對形參這個值的修改,不影響實際參數的值
引用傳遞
也稱為 地址傳遞
, 址傳遞
。方法調用時,實際參數的引用(地址,而不是參數的值)被傳遞給方法中相對應的形式參數,函數接收的是原始值的內存地址 在方法執行中,形參和實參內容相同,指向同一塊內存地址,方法執行中對引用的操作將會影響到實際對象 通過例子來說話:
-
staticclassPerson{
-
int age;
-
Person(int age){
-
this.age = age;
-
}
-
}
-
-
privatestaticvoid test(){
-
int a =100;
-
testValueT(a);
-
System.out.println("a="+ a);
-
Person person =newPerson(20);
-
testReference(person);
-
System.out.println("person.age="+ person.age);
-
}
-
-
privatestaticvoid testValueT(int a){
-
a =200;
-
System.out.println("int testValueT a="+ a);
-
}
-
-
privatestaticvoid testReference(Person person){
-
person.age =10;
-
}
輸出:
-
int testValueT a=200
-
a=100
-
person.age=10
看見 值傳遞
a的值並沒有改變,而 引用傳遞
的 persion.age已經改變了 有人說
-
privatestaticvoid testReference(Person person){
-
person =newPerson(100);
-
}
為什么 輸出的 person.age 還是20呢?
我想說 了解一下什么是 引用類型
吧? 方法內把 形參
的地址引用換成了另一個對象,並沒有改變這個對象,並不能影響 外邊 實參
還引用原來的對象,因為 形參只在方法內有效哦。
有人或許還有疑問,按照文章開頭的例子, Integer
也是 引用類型
該當如何呢? 其實 類似的 String
, Integer
, Float
, Double
, Short
, Byte
, Long
, Character
等等基本包裝類型類。因為他們本身沒有提供方法去改變內部的值,例如 Integer
內部有一個 value
來記錄 int
基本類型的值,但是沒有提供修改它的方法,而且 也是 final
類型的,無法通過 常規手段
更改。
所以雖然他們是 引用類型
的,但是我們可以認為它是 值傳遞
,這個也只是 認為
,事實上還是 引用傳遞
, 址傳遞
。
好了,基礎知識補充完畢,然我們回到面試題吧
回歸正題
-
privatestaticvoid swap(Integer numa,Integer numb){
-
Integer tmp = numa;
-
numa = numb;
-
numb = tmp;
-
System.out.println("numa="+ numa +",numb="+ numb);
-
}
通過補習基礎知識,我們很明顯知道 上面這個方法實現替換 是不可行的。因為 Interger
雖然是 引用類型
但是上述操作只是改變了 形參
的引用,而沒有改變 實參
對應的 對象
。
那么思路來了,我們 通過特殊手段
改變 Integer
內部的 value
屬性
-
privatestaticvoid swap(Integer numa,Integer numb){
-
Integer tmp = numa;
-
try{
-
Field field =Integer.class.getDeclaredField("value");
-
field.setAccessible(true);
-
field.set(numa, numb);//成功的將numa 引用的 1的對象 值改為 2
-
field.set(numb, tmp);//由於 tmp 也是指向 numa 未改變前指向的堆 即對象1 ,經過前一步,已經將對象1的值改為了2,自然 numb 也是2,所以改動失效
-
}catch(Exception e){
-
e.printStackTrace();
-
}
-
}
輸出結果:java a=1,b=2a=2,b=2
又來疑問了?為何 a
的值改變成功,而 b
的改變失敗呢?
見代碼注釋 所以其實 field.set(numb,tmp);
是更改成功的,只是 tmp 經過前一行代碼的執行,已經變成了 2。 那么如何破呢? 我們有了一個思路,既然是 tmp
的引用的對象值變量,那么我讓 tmp
不引用 numa
了
-
privatestaticvoid swap(Integer numa,Integer numb){
-
int tmp = numa.intValue();//tmp 定義為基本數據類型
-
try{
-
Field field =Integer.class.getDeclaredField("value");
-
field.setAccessible(true);
-
field.set(numa, numb);//這個時候並不改變 tmp 的值
-
field.set(numb, tmp);
-
}catch(Exception e){
-
e.printStackTrace();
-
}
-
}
這種情況下 對 numa
這個對象的修改就不會導致 tmp
的值變化了,看一下運行結果
-
a=1,b=2
-
a=2,b=2
這是為啥?有沒有 快瘋
啦? 難道我們的思路錯了? 先別着急,我們看看這個例子: 僅僅是將前面的例子 a
的值改為 129, b
的值改為130
-
publicstaticvoid main(String[] args){
-
Integer a =129;
-
Integer b =130;
-
-
System.out.println("a="+ a +",b="+ b);
-
swap(a, b);
-
System.out.println("a="+ a +",b="+ b);
-
}
-
-
privatestaticvoid swap(Integer numa,Integer numb){
-
int tmp = numa.intValue();
-
try{
-
Field field =Integer.class.getDeclaredField("value");
-
field.setAccessible(true);
-
field.set(numa, numb);
-
field.set(numb, tmp);
-
}catch(Exception e){
-
e.printStackTrace();
-
}
-
}
運行結果:
-
a=129,b=130
-
a=130,b=129
有沒有 懷疑人生
?我們的思路沒有問題啊?為什么 換個數值就行了呢? 我們稍微修改一下程序
-
publicstaticvoid main(String[] args){
-
Integer a =newInteger(1);
-
Integer b =newInteger(2);
-
-
System.out.println("a="+ a +",b="+ b);
-
swap(a, b);
-
System.out.println("a="+ a +",b="+ b);
-
}
-
-
privatestaticvoid swap(Integer numa,Integer numb){
-
int tmp = numa.intValue();
-
try{
-
Field field =Integer.class.getDeclaredField("value");
-
field.setAccessible(true);
-
field.set(numa, numb);
-
field.set(numb, tmp);
-
}catch(Exception e){
-
e.printStackTrace();
-
}
-
}
運行結果:
-
a=1,b=2
-
a=2,b=1
哎?為啥 1 和 2 也可以了?
我們這時肯定猜想和 Integer
的裝箱 拆箱有關
裝箱,拆箱 概念
Integer的裝箱操作
為什么 Integera=1
和 Integera=newInteger(1)
效果不一樣 那就瞅瞅源碼吧?
-
publicInteger(int value){
-
this.value = value;
-
}
-
-
/**
-
* Returns an {@code Integer} instance representing the specified
-
* {@code int} value. If a new {@code Integer} instance is not
-
* required, this method should generally be used in preference to
-
* the constructor {@link #Integer(int)}, as this method is likely
-
* to yield significantly better space and time performance by
-
* caching frequently requested values.
-
*
-
* This method will always cache values in the range -128 to 127,
-
* inclusive, and may cache other values outside of this range.
-
*
-
* @param i an {@code int} value.
-
* @return an {@code Integer} instance representing {@code i}.
-
* @since 1.5
-
*/
-
publicstaticInteger valueOf(int i){
-
if(i >=IntegerCache.low && i <=IntegerCache.high)
-
returnIntegerCache.cache[i +(-IntegerCache.low)];
-
returnnewInteger(i);
-
}
通過注釋知道,java推薦 Integer.valueOf
方式初始化一個 Interger
因為有 緩存了 -128-127
的數字 我們直接定義 Integera=1
具有這個功能,所以 Jvm 底層實現 是通過 Integer.valueOf
這個方法 再看 field.set(numb,tmp);
我們打斷點,發現通過反射設置 value
時 竟然走了 Integer.valueOf
方法 下面是 我們調用 swap
前后的 IntegerCache.cache
值得變化
反射修改前:
反射修改后
在反射修改前
-
IntegerCache.cache[128]=0
-
IntegerCache.cache[129]=1
-
IntegerCache.cache[130]=2
通過反射修改后
-
IntegerCache.cache[128]=0
-
IntegerCache.cache[129]=2
-
IntegerCache.cache[130]=2
再調用 field.set(numb,tmp)
tmp這時等於1 對應的 角標 129 ,但是這個值已經變成了2 所以出現了剛才 奇怪的結果
原來都是 緩存的鍋
下面趁機再看個例子 加深理解
-
Integer testA =1;
-
Integer testB =1;
-
-
Integer testC =128;
-
Integer testD =128;
-
System.out.println("testA=testB "+(testA == testB)+",\ntestC=testD "+(testC == testD));
輸出結果:
java testA=testBtrue,testC=testDfalse
通過這小示例,在 -128 到 127的數字都走了緩存,這樣 testA
和 testB
引用的是同一片內存區域的同一個對象。 而 testC
testD
數值大於127 所以 沒有走緩存,相當於兩個 Integer
對象,在堆內存區域有兩個對象。 兩個對象自如不相等。
在前面的示例中 我們 通過
-
Integer a =newInteger(1);
-
Integer b =newInteger(2);
方式初始化 a
, b
我們的交換算法沒有問題,也是這個原因。
那么到目前為止我們的 swap
方法可以完善啦
-
privatestaticvoid swap(Integer numa,Integer numb){
-
int tmp = numa.intValue();
-
try{
-
Field field =Integer.class.getDeclaredField("value");
-
field.setAccessible(true);
-
field.set(numa, numb);
-
field.set(numb,newInteger(tmp));
-
}catch(Exception e){
-
e.printStackTrace();
-
}
-
}
只需將之前的 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類型了,就不會再拆箱后再裝箱。