簡介
最近有點忙,很久沒更新文章了,後面會慢慢恢復...回顧正題
最近看到一篇文章,關於一道面試題,先看一下題目,如下:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.printf("a = %s, b = %s\n", a, b);
swap(a, b);
System.out.printf("a = %s, b = %s\n", a, b);
}
public static void swap(Integer a, Integer b) {
// TODO 實現
}複製程式碼
有人可能在沒經過仔細考慮的情況下,給出以下的答案
// 特別提醒,這是錯誤的方式
// 特別提醒,這是錯誤的方式
// 特別提醒,這是錯誤的方式
public static void swap(Integer a, Integer b) {
// TODO 實現
Integer temp = a;
a = b;
b = temp;
}複製程式碼
很遺憾,這是錯誤的。重要的事註釋三遍
那麼為什麼錯誤,原因是什麼?
想要搞清楚具體的原因,在這裡你需要搞清楚以下幾個概念,如果這個概念搞清楚了,你也不會把上面的實現方法寫錯
- 形參和實參
- 引數值傳遞
- 自動裝箱
所以,上面的問題先放一邊,先看一下這幾個概念
形參和實參
什麼是形參?什麼是實參?概念上的東西,參考教科書或者google去吧,下面直接程式碼說明更加明顯
public void test() {
int shi_can = 0;
testA(shi_can);
}
public void testA(int xing_can) {
}複製程式碼
注:為了清楚的表達意思,我命名的時候並沒有按照java的駝峰規則命名,這裡只是為了演示複製程式碼
通過上面的程式碼很清楚的表達形參和實參的概念,在呼叫testA時,傳遞的就是實參,而在testA方法簽名中的引數為形參
從作用域上看,形參只會在方法內部生效,方法結束後,形參也會被釋放掉,所以形參是不會影響方法外的
值傳遞和引用傳遞
值傳遞:傳遞的是實際值,像基本資料型別
引用傳遞:將物件的引用作為實參進行傳遞
java基本型別資料作為引數是值傳遞,物件型別是引用傳遞
實參是可以傳遞給形參的,但是形參卻不能影響實參,所以,當進行值傳遞的情況下,改變的是形參的值,並沒有改變實參,所以無論是引用傳遞還是值傳遞,只要更改的是形參本身,那麼都無法影響到實參的。對於引用傳遞而言,不同的引用可以指向相同的地址,通過形參的引用地址,找到了實際物件分配的空間,然後進行更改就會對實參指向的物件產生影響
額,上面表述,可能有點繞,看程式碼
// 僅僅是一個java物件
public class IntType {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
// main方法
public class IntTypeSwap {
public static void main(String[] args) {
// CODE_1
IntType type1 = new IntType();
type1.setValue(1);
IntType type2 = new IntType();
type2.setValue(2);
// CODE_1
swap1(type1, type2);
System.out.printf("type1.value = %s, type2.value = %s", type1.getValue(), type2.getValue());
swap2(type1, type2);
System.out.println();
System.out.printf("type1.value = %s, type2.value = %s", type1.getValue(), type2.getValue());
}
public static void swap2(IntType type1, IntType type2) {
int temp = type1.getValue();
type1.setValue(type2.getValue());
type2.setValue(temp);
}
public static void swap1(IntType type1, IntType type2) {
IntType type = type1;
type1 = type2;
type2 = type;
}
}複製程式碼
在main方法中,CODE_1中間的程式碼為宣告瞭兩個物件,分別設定value為1和2,而swap1和swap2兩個方法的目的是為了互動這兩個物件的value值
先思考一下,應該輸出的結果是什麼
...
...
type1.value = 1, type2.value = 2
type1.value = 2, type2.value = 1複製程式碼
從輸出結果來看swap1並沒有達到目的,回頭看一下swap1
public static void swap1(IntType type1, IntType type2) {
IntType type = type1;
type1 = type2;
type2 = type;
}複製程式碼
從值傳遞的角度來看,物件引數傳遞採用的是引用傳遞,那麼type1和type2傳遞過來的是指向物件的引用,在方法內部,直接操作形參,交換了形參的內容,這樣形參改變,都是並沒有對實參產生任何影響,也沒有改變物件實際的值,所以,結果是無法交換
而對於swap2,物件引用作為形參傳遞過來後,並沒有對形參做任何的改變,而是直接操作了形參所指向的物件實際地址,那這樣,無論是實參還是其他地方,只要是指向該物件的所有的引用地址對應的值都會改變
自動裝箱
看我上面的那個例子的swap1,是不是頓時覺得與上面的面試題的錯誤做法非常相似了,是的,錯誤的原因是一模一樣的,就是稍微有一點區別,就是Integer不是new出來的,而是自動裝箱的一個物件,那麼什麼是自動裝箱呢?jdk到底做了什麼事?
如果你不想知道為什麼,只想知道結果,那麼我就直說,自動裝箱就是jdk呼叫了Integer的valueOf(int)的方法,很簡單,看原始碼
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}複製程式碼
上面那些如果不想深究可以忽略,就看最後一句,是不是明白了什麼呢。沒錯,也是new出來一個物件,如果想知道上面的程式碼做了什麼處理,可以參考 Long==Long有趣的現象 這篇文章,裡面有介紹類似的
好了,有人可能會問,為什麼會知道自動裝箱呼叫的是valueOf方法,這裡其他人怎麼知道的我不清楚,我是通過檢視反編譯的位元組碼指令知道的
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.printf("a = %s, b = %s\n", a, b);
swap(a, b);
System.out.printf("a = %s, b = %s\n", a, b);
}
public static void swap(Integer a, Integer b) {
Integer temp = a;
a = b;
b = temp;
}複製程式碼
反編譯出來的結果為
對比一下可以很清楚的看到valueOf(int)方法被呼叫
迴歸
好,現在迴歸正題了,直接操作形參無法改變實際值,而Integer又沒有提供set方法,那是不是無解了呢?我很好奇如果有人以下這樣寫,面試官會有什麼反應
public static void swap(Integer a, Integer b) {
// TODO 實現
// 無解,
}複製程式碼
既然出了肯定是有解的,可以實現,回頭看看,在上面swap2的那個例子中是通過set方法來改變值的,那麼Integer有沒有提供呢?答案沒有(我沒找到)
那就先看看原始碼
private final int value;
...
public Integer(int value) {
this.value = value;
}複製程式碼
這是Integer的建構函式,可以看到Integer物件實際值是用value屬性來儲存的,但是這個value是被final修飾的,沒辦法繼續找,value沒有提供任何的set方法。既然在萬法皆不通的情況下,那就只能動用反射來解決問題
public static void swap(Integer a, Integer b) {
int temp = a.intValue();
try {
Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
value.set(a, b);
value.set(b, temp);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}複製程式碼
現在感覺很開心,終於找到解決方案,可是當你執行的時候,從輸出結果你會發現,jdk在跟我開玩笑嗎
a = 1, b = 2
a = 2, b = 2複製程式碼
為什麼會出現這種情況,無奈,除錯會發現是在value.set的時候將Integer的快取值改變了,因為value.set(Object v1, Object v2)兩個引數都是物件型別,所以temp會進行自動裝箱操作,會呼叫valueOf方法,這樣會獲取到錯誤的快取值,所以,為了避免這種情況,就只能不需要呼叫快取值,直接new Integer就可以跳過快取,所以程式碼改成如下即可
public static void swap(Integer a, Integer b) {
int temp = a.intValue();
try {
Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
value.set(a, b);
value.set(b, new Integer(temp));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}複製程式碼
至此,這道題完美結束