以為很簡單的 int (1) 和 int (10),卻成了面試滑鐵盧!
兄弟們,有沒有過這樣的經(jīng)歷?面試的時(shí)候,面試官突然拋出一個看似簡單到不能再簡單的問題:"說說 int 和 Integer 的區(qū)別,再看看這段代碼的輸出結(jié)果是什么?" 然后在白板上寫下兩行代碼:
Integer a = 1;
Integer b = 10;
System.out.println(a == b);
System.out.println(a.equals(b));
你心里暗自竊喜,這不就是自動裝箱嘛,int 和 Integer 的區(qū)別早就滾瓜爛熟了。可是當(dāng)你自信滿滿地說出 "第一個輸出 false,第二個輸出 true" 的時(shí)候,面試官嘴角上揚(yáng),露出一絲神秘的微笑:"那如果是這樣呢?" 接著又寫下:
Integer c = 1;
Integer d = 1;
System.out.println(c == d);
Integer e = 128;
Integer f = 128;
System.out.println(e == f);
這時(shí)候你突然意識到事情沒那么簡單,剛才的答案可能有問題。看著面試官似笑非笑的表情,你開始懷疑人生:明明都是 int 裝箱成 Integer,為什么有的用 == 比較是 true,有的又是 false?難道 1 和 10 有什么特殊魔力?今天咱們就來好好掰扯掰扯這個讓無數(shù)程序員在面試中翻車的 "簡單" 問題,看看背后藏著多少不為人知的細(xì)節(jié)。
一、從自動裝箱說起:編譯器背后的小魔術(shù)
首先,我們得搞清楚 int 和 Integer 之間的關(guān)系。在 Java 5 之后,引入了自動裝箱(Autoboxing)和自動拆箱(Unboxing)的特性,讓基本數(shù)據(jù)類型和對應(yīng)的包裝類之間可以自動轉(zhuǎn)換。比如說:
Integer x = 5; // 自動裝箱,相當(dāng)于Integer x = Integer.valueOf(5);
int y = x; // 自動拆箱,相當(dāng)于int y = x.intValue();
這個特性讓我們在編寫代碼時(shí)可以更方便地使用包裝類,不用頻繁地手動調(diào)用 valueOf () 和 xxxValue () 方法。但是,自動裝箱并不是簡單地把 int 包裝成 Integer 對象,背后涉及到一個重要的方法 ——Integer.valueOf(int i)。這個方法可是大有學(xué)問,面試題的玄機(jī)就藏在這里。我們先來看一下Integer.valueOf(int i)的源碼(以 Java 8 為例):
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
看到?jīng)]?這里有一個IntegerCache的緩存機(jī)制。當(dāng)傳入的 int 值在IntegerCache.low和IntegerCache.high之間時(shí),不會創(chuàng)建新的 Integer 對象,而是直接從緩存中獲取已經(jīng)存在的對象。而默認(rèn)情況下,IntegerCache.low是 - 128,IntegerCache.high是 127。也就是說,當(dāng)我們將一個 int 值裝箱成 Integer 時(shí),如果值在 - 128 到 127 之間,會直接返回緩存中的對象,而不是新建一個對象;如果超過這個范圍,才會新建一個 Integer 對象。這下明白了吧?剛才的例子中,Integer c = 1和Integer d = 1,因?yàn)?1 在緩存范圍內(nèi),所以 c 和 d 指向的是同一個緩存中的對象,用 == 比較自然是 true;而Integer e = 128和Integer f = 128,128 超過了默認(rèn)的緩存上限 127,所以會新建兩個不同的 Integer 對象,用 == 比較就是 false。
但是等等,這里有個問題:面試官剛才的第一個問題中,a 是 1,b 是 10,都是在緩存范圍內(nèi),為什么a == b是 false 呢?哦,對了,因?yàn)?a 和 b 是不同的對象,雖然都在緩存范圍內(nèi),但緩存的是相同值的對象,而不是不同值的對象。也就是說,緩存是針對單個值的,每個值在緩存中只有一個對象。所以 1 對應(yīng)的緩存對象和 10 對應(yīng)的緩存對象是不同的,所以 a 和 b 指向不同的對象,== 比較自然是 false,而 equals 比較的是值,所以是 true。
二、Integer 緩存機(jī)制:面試官挖的第一個坑
剛才提到的IntegerCache是 Java 中為了優(yōu)化性能而引入的一個緩存機(jī)制,用于緩存常用的小整數(shù)對象,避免頻繁創(chuàng)建和銷毀對象帶來的性能開銷。這個緩存的范圍默認(rèn)是 - 128 到 127,但是我們可以通過 JVM 參數(shù)來修改這個范圍。比如,在啟動程序時(shí)加上-XX:AutoBoxCacheMax=200,就可以將緩存的上限設(shè)置為 200,這樣 200 以內(nèi)的整數(shù)裝箱時(shí)都會使用緩存中的對象。
不過,需要注意的是,這個緩存機(jī)制只適用于自動裝箱的情況,也就是通過Integer.valueOf(int i)方法來獲取 Integer 對象的情況。如果我們直接使用 new Integer (int i) 來創(chuàng)建對象,不管值是多少,都會新建一個新的對象,不會使用緩存。比如:
Integer g = new Integer(1);
Integer h = new Integer(1);
System.out.println(g == h); // 輸出false,因?yàn)槊看蝞ew都會創(chuàng)建新對象
System.out.println(g.equals(h)); // 輸出true,因?yàn)橹迪嗤?/code>
另外,還有一個容易混淆的地方是,Integer 的緩存機(jī)制是在類加載的時(shí)候就已經(jīng)初始化好了的,也就是說,當(dāng)我們第一次使用 Integer 類的時(shí)候,緩存就已經(jīng)創(chuàng)建好了,包含 - 128 到 127 之間的所有整數(shù)對象。所以,不管我們在程序的哪個地方裝箱一個在這個范圍內(nèi)的整數(shù),都會返回同一個緩存中的對象。這里還有一個有趣的現(xiàn)象:當(dāng)我們將一個 Integer 對象賦值給 int 變量時(shí),會發(fā)生自動拆箱,這時(shí)候比較的是值而不是對象引用。比如:
Integer i = 1;
int j = 1;
System.out.println(i == j); // 輸出true,因?yàn)樽詣硬鹣浜蟊容^的是值
這是因?yàn)楫?dāng)一個 Integer 對象和一個基本數(shù)據(jù)類型 int 進(jìn)行比較時(shí),Integer 會自動拆箱成 int,然后比較兩個 int 的值,所以結(jié)果是 true。
三、== vs equals:面試官挖的第二個坑
接下來,我們來深入探討一下 == 和 equals 方法的區(qū)別。這是 Java 面試中非常經(jīng)典的問題,但很多人對它們的理解還停留在表面。
首先,== 對于基本數(shù)據(jù)類型來說,比較的是值是否相等;對于引用數(shù)據(jù)類型來說,比較的是對象的內(nèi)存地址是否相同,也就是是否指向同一個對象。
而 equals 方法是 Object 類的一個實(shí)例方法,默認(rèn)實(shí)現(xiàn)也是比較對象的內(nèi)存地址,和 == 的效果一樣。但是,很多類重寫了 equals 方法,比如 String、Integer 等,重寫后的 equals 方法比較的是對象的內(nèi)容是否相等。
以 Integer 為例,它的 equals 方法源碼如下:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
可以看到,Integer 的 equals 方法會先判斷對象是否是 Integer 類型,如果是,就比較包裝的 int 值是否相等。所以,當(dāng)我們用 equals 比較兩個 Integer 對象時(shí),只要它們包裝的 int 值相同,就會返回 true,不管它們是否是同一個對象。但是,這里需要注意一個問題:如果我們將一個 null 值和一個 Integer 對象用 equals 比較,會拋出 NullPointerException。而用 == 比較的話,null 和任何對象引用比較都是 false,不會拋出異常。
另外,還有一種常見的錯誤是,誤以為所有包裝類的 equals 方法都和 Integer 一樣,只比較值。其實(shí)不然,比如 Double 和 Float 的 equals 方法在比較時(shí),還會考慮 NaN 的情況。不過,這是另一個話題,我們今天先聚焦在 Integer 上。
回到最初的面試題,當(dāng)面試官問a == b和a.equals(b)的結(jié)果時(shí),我們需要分情況討論:
- 如果 a 和 b 都是通過自動裝箱(即 Integer.valueOf ())得到的,并且值在緩存范圍內(nèi)(-128 到 127),那么當(dāng)值相同時(shí),a == b 為 true,否則為 false;而 a.equals (b) 只要值相同就為 true。
- 如果 a 和 b 是通過 new Integer () 創(chuàng)建的,那么不管值是否相同,a == b 永遠(yuǎn)為 false,因?yàn)槊看?new 都會創(chuàng)建新對象;而 a.equals (b) 只要值相同就為 true。
- 當(dāng)一個是基本類型 int,一個是 Integer 對象時(shí),== 比較會自動拆箱,比較值是否相同;而 equals 比較時(shí),因?yàn)?int 會自動裝箱成 Integer,所以和兩個 Integer 對象比較一樣,比較值是否相同。
四、哈希碼與 equals:面試官可能追問的第三個坑
在 Java 中,哈希碼(hash code)和 equals 方法有著密切的關(guān)系。根據(jù) Java 的規(guī)范,兩個對象如果 equals 方法返回 true,那么它們的哈希碼(hashCode () 方法的返回值)必須相等;如果 equals 方法返回 false,它們的哈希碼可以相等也可以不相等。
Integer 類重寫了 hashCode 方法,返回的是包裝的 int 值。所以,兩個值相同的 Integer 對象,它們的哈希碼是相等的,這符合上述規(guī)范。
我們可以通過一個例子來驗(yàn)證:
Integer k = 100;
Integer l = 100;
System.out.println(k.hashCode()); // 輸出100
System.out.println(l.hashCode()); // 輸出100
System.out.println(k.equals(l)); // 輸出true
Integer m = new Integer(100);
System.out.println(m.hashCode()); // 輸出100,因?yàn)橹貙懥薶ashCode方法,返回值本身
這里需要注意的是,如果我們自定義一個類,重寫了 equals 方法,就必須同時(shí)重寫 hashCode 方法,否則可能會違反上述規(guī)范,導(dǎo)致在使用哈希表(如 HashMap、HashSet)時(shí)出現(xiàn)問題。不過,這是另一個層面的問題,我們今天主要關(guān)注 Integer 類。另外,當(dāng)我們將 Integer 對象作為 HashMap 的鍵時(shí),需要注意如果對象被修改了(雖然 Integer 是不可變類,值不會被修改,但如果是自定義的可變類),哈希碼可能會改變,導(dǎo)致無法正確獲取對應(yīng)的 value。不過,Integer 是不可變的,所以不用擔(dān)心這個問題,但這是一個需要了解的知識點(diǎn)。
五、序列化與反序列化:面試官可能深挖的第四個坑
Integer 作為 Java 的基本包裝類,實(shí)現(xiàn)了 Serializable 接口,所以可以被序列化和反序列化。在序列化過程中,Integer 對象會被轉(zhuǎn)換成字節(jié)流,反序列化時(shí)再恢復(fù)成對象。
這里有一個有趣的現(xiàn)象:當(dāng)反序列化一個 Integer 對象時(shí),返回的對象是否來自緩存呢?答案是肯定的。因?yàn)榉葱蛄谢^程中,會調(diào)用 Integer.valueOf () 方法來創(chuàng)建對象,所以如果值在緩存范圍內(nèi),會返回緩存中的對象,而不是新建一個對象。
我們可以通過一個簡單的例子來驗(yàn)證:
import java.io.*;
publicclass IntegerSerializationTest {
public static void main(String[] args) throws Exception {
Integer n = 100;
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("integer.ser"));
oos.writeObject(n);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("integer.ser"));
Integer m = (Integer) ois.readObject();
ois.close();
System.out.println(n == m); // 輸出true,因?yàn)?00在緩存范圍內(nèi),反序列化使用了valueOf方法
}
}
運(yùn)行結(jié)果是 true,說明反序列化得到的 Integer 對象和緩存中的對象是同一個。而如果序列化的值是 128,反序列化得到的對象和新裝箱的 128 是否是同一個呢?我們來試一下:
Integer p = 128;
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("integer2.ser"));
oos2.writeObject(p);
oos2.close();
ObjectInputStream ois2 = new ObjectInputStream(new FileInputStream("integer2.ser"));
Integer q = (Integer) ois2.readObject();
ois2.close();
System.out.println(p == q); // 輸出false,因?yàn)?28不在默認(rèn)緩存范圍內(nèi),序列化時(shí)保存的是對象的二進(jìn)制數(shù)據(jù),反序列化時(shí)調(diào)用valueOf方法,128超過緩存上限,新建對象
System.out.println(p.equals(q)); // 輸出true
這里輸出 false,因?yàn)?128 不在默認(rèn)的緩存范圍內(nèi),反序列化時(shí)會調(diào)用 Integer.valueOf (128),而該方法會新建一個 Integer 對象,所以 p 和 q 是不同的對象,但值相同。
六、擴(kuò)展思考:其他包裝類的緩存機(jī)制
其實(shí),不僅僅是 Integer 類有緩存機(jī)制,Java 中的其他基本包裝類,如 Byte、Short、Long、Character 等,也都有類似的緩存機(jī)制,只不過緩存的范圍可能不同:
- Byte:緩存范圍是 - 128 到 127,因?yàn)?Byte 的取值范圍就是 - 128 到 127,所以所有值都會被緩存。
- Short:默認(rèn)緩存范圍是 - 128 到 127,可以通過 JVM 參數(shù)修改上限。
- Long:默認(rèn)緩存范圍是 - 128 到 127,可以通過 JVM 參數(shù)修改上限。
- Character:默認(rèn)緩存范圍是 0 到 127,因?yàn)?Character 表示的是 Unicode 字符,0 到 127 對應(yīng) ASCII 字符,是比較常用的范圍。
- Double 和 Float:沒有緩存機(jī)制,因?yàn)楦↑c(diǎn)數(shù)的范圍太大,而且存在精度問題,緩存沒有意義。
我們以 Short 為例,來看一下它的 valueOf 方法源碼:
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
可以看到,Short 的緩存范圍也是 - 128 到 127,和 Integer 類似。而 Character 的緩存范圍是 0 到 127,源碼如下:
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
所以,當(dāng)我們使用這些包裝類時(shí),也要注意它們的緩存機(jī)制,避免在面試中被問到類似的問題時(shí)翻車。
七、面試陷阱總結(jié):這些坑你都踩過嗎?
現(xiàn)在,我們來總結(jié)一下面試中關(guān)于 int 和 Integer 的常見問題和陷阱:
- 自動裝箱 / 拆箱的原理:知道是通過 valueOf () 和 xxxValue () 方法實(shí)現(xiàn)的,尤其是 valueOf () 方法的緩存機(jī)制。
- == 和 equals 的區(qū)別:基本類型比較值,引用類型比較地址;equals 方法在 Integer 中比較的是值,但要注意 null 的情況。
- Integer 緩存范圍:默認(rèn) - 128 到 127,可通過 JVM 參數(shù)修改,new Integer () 不會使用緩存。
- 哈希碼與 equals 的關(guān)系:重寫 equals 必須重寫 hashCode,Integer 的 hashCode 返回值本身。
- 序列化問題:反序列化使用 valueOf () 方法,所以在緩存范圍內(nèi)會返回緩存對象。
- 其他包裝類的緩存:Byte、Short、Long、Character 有緩存,Double 和 Float 沒有。
為了幫助大家更好地理解,我們再來看幾個經(jīng)典的面試題例子:
例子 1:
Integer a1 = 127;
Integer a2 = 127;
System.out.println(a1 == a2); // 輸出true,127在緩存范圍內(nèi)
Integer b1 = 128;
Integer b2 = 128;
System.out.println(b1 == b2); // 輸出false,128超出緩存范圍
例子 2:
Integer c1 = new Integer(100);
Integer c2 = new Integer(100);
System.out.println(c1 == c2); // 輸出false,new創(chuàng)建新對象
System.out.println(c1.equals(c2)); // 輸出true,值相同
例子 3:
Integer d1 = 100;
int d2 = 100;
System.out.println(d1 == d2); // 輸出true,d1自動拆箱成int,比較值
例子 4:
Integer e1 = null;
Integer e2 = 100;
// System.out.println(e1 == e2); // 輸出false,不會拋異常
// System.out.println(e1.equals(e2)); // 拋NullPointerException
例子 5:
Integer f1 = Integer.valueOf(100);
Integer f2 = Integer.valueOf(100);
System.out.println(f1 == f2); // 輸出true,使用緩存對象
八、為什么面試官喜歡問這個問題?
看到這里,可能有人會問:不就是一個自動裝箱和緩存的問題嗎?為什么面試官總是揪著不放?其實(shí),這個問題雖然看似簡單,但背后涉及到 Java 的很多核心概念:
- 基本類型與包裝類的區(qū)別:值類型和引用類型的本質(zhì)區(qū)別,棧內(nèi)存和堆內(nèi)存的存儲方式。
- 自動裝箱拆箱的實(shí)現(xiàn)原理:理解編譯器如何處理基本類型和包裝類的轉(zhuǎn)換,背后的方法調(diào)用。
- 對象池技術(shù)(緩存機(jī)制):Java 中為了優(yōu)化性能而采用的常見技術(shù),如 String 常量池、Integer 緩存池等,理解性能優(yōu)化的思路。
- == 和 equals 的語義:深入理解 Java 中對象比較的規(guī)則,避免在實(shí)際開發(fā)中出現(xiàn)邏輯錯誤。
- 不可變類的設(shè)計(jì):Integer 是不可變類,一旦創(chuàng)建值就不能改變,理解不可變類的優(yōu)點(diǎn)和應(yīng)用場景。
這些知識點(diǎn)都是 Java 程序員必須掌握的基礎(chǔ),尤其是在涉及到對象比較、集合操作(如 HashMap 的鍵)、性能優(yōu)化等場景時(shí),對這些細(xì)節(jié)的理解會直接影響代碼的正確性和效率。
九、實(shí)際開發(fā)中的注意事項(xiàng)
雖然面試中經(jīng)常考察這些細(xì)節(jié),但在實(shí)際開發(fā)中,我們應(yīng)該如何正確使用 int 和 Integer 呢?
- 優(yōu)先使用基本類型:如果不需要對象功能(如 null 值、方法調(diào)用等),優(yōu)先使用 int、double 等基本類型,因?yàn)樗鼈兏咝В加脙?nèi)存更小。
- 注意 null 值處理:當(dāng)使用 Integer 時(shí),要注意可能為 null 的情況,避免空指針異常。比如,在數(shù)據(jù)庫查詢中,整數(shù)類型的字段可能返回 null,這時(shí)候需要合理處理。
- 謹(jǐn)慎使用 == 比較對象:除非你確定兩個引用指向同一個對象(如來自緩存或同一個 new 操作),否則應(yīng)該使用 equals 方法比較值,尤其是在涉及自動裝箱的情況下。
- 了解框架的處理方式:很多框架(如 Spring、MyBatis)在處理數(shù)據(jù)類型轉(zhuǎn)換時(shí),會涉及到自動裝箱拆箱,了解這些機(jī)制可以幫助我們更好地調(diào)試和優(yōu)化代碼。
- 性能敏感場景的優(yōu)化:在高頻調(diào)用的代碼中,如循環(huán)內(nèi)部,如果需要創(chuàng)建大量小整數(shù)的 Integer 對象,使用自動裝箱(利用緩存)會比 new Integer () 更高效,因?yàn)楸苊饬藢ο髣?chuàng)建和垃圾回收的開銷。
十、總結(jié):細(xì)節(jié)決定成敗
回到最初的面試場景,為什么一個看似簡單的 int 和 Integer 的問題會成為滑鐵盧?因?yàn)楹芏喑绦騿T只停留在表面知識,知道自動裝箱拆箱,知道 == 和 equals 的區(qū)別,但沒有深入理解背后的實(shí)現(xiàn)原理,尤其是 Integer 的緩存機(jī)制。而面試官通過這個問題,實(shí)際上是在考察候選人對 Java 基礎(chǔ)的掌握程度,是否注重細(xì)節(jié),是否有深入鉆研的習(xí)慣。
技術(shù)面試的本質(zhì),不是考察你會不會某個冷門的 API,而是考察你對基礎(chǔ)原理的理解深度,以及能否將這些原理應(yīng)用到實(shí)際開發(fā)中。就像 Integer 的緩存機(jī)制,看似只是一個小細(xì)節(jié),但背后涉及到性能優(yōu)化、對象池設(shè)計(jì)、語言特性實(shí)現(xiàn)等多個層面的知識。
所以,各位程序員朋友們,下次遇到類似的問題,不要輕敵,多問自己幾個為什么:為什么會有自動裝箱?為什么 Integer 要設(shè)計(jì)緩存機(jī)制?緩存范圍為什么是 - 128 到 127?修改 JVM 參數(shù)會有什么影響?只有把這些問題都搞清楚,才能在面試中從容應(yīng)對,避免滑鐵盧。