點贊關(guān)注蚤假,不再迷路,你的支持對我意義重大吊说!
?? Hi论咏,我是丑丑。本文 「Java 路線」導(dǎo)讀 —— 他山之石颁井,可以攻玉 已收錄厅贪,這里有 Android 進(jìn)階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長雅宾。(聯(lián)系方式在 GitHub)
前言
面試季又來了养涮,Java 基礎(chǔ)知識又可以拿出來復(fù)習(xí)了~ “基礎(chǔ)不牢,地動山搖”,這些內(nèi)容不難贯吓,但必須要會懈凹,一起加油吧。
1. 誤區(qū)
Java 是解釋型語言還是編譯型語言悄谐?
所謂 “某個語言是編譯型 / 解釋型” 是個偽命題介评,編譯型和解釋型并不是一門編程語言的特性,而是語言實現(xiàn)的特性尊沸。我們經(jīng)常會在文章或教科書上看到 “C 是編譯型語言威沫,因為 C 是編譯執(zhí)行的;Java 是解釋型語言洼专,因為 Java 是 JVM 解釋執(zhí)行的”棒掠,這些都是不準(zhǔn)確的。更準(zhǔn)確的說法是:“某個語言的特定實現(xiàn)是編譯型還是解釋型”屁商。
舉個例子烟很,在早期的 Android Dalvik 虛擬機(jī)只有解釋器,效率低蜡镶。為了優(yōu)化運行效率雾袱,Android 2.2 引入 JIT 即時編譯器,可以在運行時探測熱點代碼進(jìn)行編譯執(zhí)行官还。后續(xù)的 Android 5.0 ART 虛擬機(jī)又推出了 AOT 提前編譯芹橡,Android 7/0 ART 又引入了解釋、JIT 和 AOT 混合的執(zhí)行方式望伦×炙担可以看到,同一段代碼既可以解釋執(zhí)行屯伞,也可以編譯執(zhí)行腿箩,這取決于語言的實現(xiàn)而不是語言本身。
相關(guān)深入文章:《Android 虛擬機(jī) | 從類加載到程序執(zhí)行》
C 語言是面向過程語言劣摇,Java 是面向?qū)ο缶幊陶Z言珠移?
所謂 “某種語言是面向過程 / 面向?qū)ο?/ 函數(shù)式” 是一個偽命題。經(jīng)常會在文章或教科書上看到 “C 是面向過程語言末融,Java 是面向?qū)ο笳Z言钧惧,C++ 是面向?qū)ο笳Z言”,這種分類方法是沒有意義的勾习。舉個例子垢乙,使用 Java 可以使用寫出函數(shù)式風(fēng)格的代碼,也可以寫出面向?qū)ο箫L(fēng)格的代碼语卤。因此,這種分類方式對于編程沒有意義。
更科學(xué)的思考方式是把編程語言理解為一個個特性的組合 粹舵,學(xué)習(xí)一門語言應(yīng)該把語言打散為一個個特性钮孵,把每個特性都理解透徹。那么眼滤,當(dāng)你遇到一門新的語言巴席,雖然語法有所不同,但是很多特性是相似的诅需,這樣就只需要專注于兩門語言差集的特性漾唉,學(xué)習(xí)效率就高了。
Java 是靜態(tài)類型檢查還是動態(tài)類型檢查堰塌?
靜態(tài) / 動態(tài)類型語言的區(qū)分赵刑,關(guān)鍵在于類型檢查是否 (傾向于) 編譯時執(zhí)行。例如场刑, Java & C/C++ 是靜態(tài)類型檢查般此,而 JavaScript 是動態(tài)類型檢查。靜態(tài)類型檢查的優(yōu)點是可以在編譯期提前檢查出可能出現(xiàn)的 bug牵现。需要注意的是铐懊,這個定義并不是絕對的,例如 Java 也存在運行時類型檢查的方式瞎疼,例如上面提到的 checkcast 指令本質(zhì)上是在運行時檢查變量的類型與對象的類型是否相同科乎。
2. 類 & 對象
為什么 Java 匿名內(nèi)部類調(diào)用的局部變量需要聲明 final?
局部變量的作用域是從變量定義到代碼塊結(jié)束贼急,在匿名內(nèi)部類訪問局部變量茅茂,其實是超出了局部變量的作用域。為了擴(kuò)大變量的作用域竿裂,編譯后的字節(jié)碼實現(xiàn)是將局部變量的值通過構(gòu)造函數(shù)傳遞到內(nèi)部類內(nèi)部玉吁,存儲在成員變量。這就意味著局部變量的值存在兩份拷貝腻异,又因為 Java 賦值是值傳遞进副,所以當(dāng)任何一份拷貝修改時,另外一份拷貝是無感知的悔常,這會引起語義上的混亂影斑。因此,為了保證數(shù)據(jù)一致性机打,Java 規(guī)定匿名內(nèi)部類訪問的局部變量需要聲明為 final矫户。
有沒有辦法不使用 final 呢?也是有的残邀,那就是使用一層數(shù)組或者對象包裝皆辽,這正是 Kotlin lambda 表達(dá)式可以直接訪問局部變量的原理柑蛇。
== 和 equals() 有什么區(qū)別?為什么重寫 equals() 必須重寫 hashCode()驱闷?
這三者都帶有 相等 的含義耻台,但它們在表示相等的層面又各不相同:
1、== 表示值相等空另,對于基礎(chǔ)數(shù)據(jù)類型是值相等盆耽,對于引用類型是對象地址相等,本質(zhì)上也是值相等扼菠;
2摄杂、equals() 表示內(nèi)容相等,equals() 是 Object 的成員方法循榆,所以基本數(shù)據(jù)類型沒有 equals() 方法析恢。Object#equals() 的默認(rèn)實現(xiàn)時比較對象地址相等 (this == obj);
3冯痢、hashCode() 是 Object 的 native 方法氮昧,底層實現(xiàn)是將對象的內(nèi)存地址進(jìn)行哈希運算計算出的整數(shù)值;
4浦楣、 你說的是 Object.hashCode 通用約定(即:在實際中會有兩個對象相同袖肥,那么對應(yīng)的 hashCode() 一定相同,如果兩個對象 hashCode() 相同振劳,它們不一定相同(哈希沖突))椎组。這個約定是為了確保該類作為散列集合的 Key 時能夠正常運行(包括 HashMap、HashSet 和 Hashtable)历恐。如果不按照約定寸癌,也就是重寫 equals() 但未重寫 hashCode(),就會出現(xiàn)兩個 equals 的對象在哈希表中存儲了兩個獨立的鍵值對弱贼,這與哈希集合的語義矛盾蒸苇。
3、字符 & 字符串
在每種編程語言里吮旅,字符串都是一個躲不開的話題溪烤,也是面試常常出現(xiàn)的問題,關(guān)于「字符串」的面試題我統(tǒng)一整理在這篇文章:《Java | String 常見面試題》
4庇勃、程序設(shè)計
Java 如何實現(xiàn)單例模式檬嘀?
在 Java 中,有五種常規(guī)單例實現(xiàn)责嚷,每種單例實現(xiàn)各有特點鸳兽,沒有哪一種是最佳選擇。這五種實現(xiàn)是進(jìn)程級別的單例罕拂,除此之外還有線程級別單例揍异,可以利用 ThreadLocal 實現(xiàn)全陨。
先說一下五種常規(guī)單例實現(xiàn):
1、餓漢式: 線程安全蒿秦,調(diào)用效率最高烤镐,但不能懶加載
2、懶漢式 + synchronized: 線程安全棍鳖,調(diào)用效率不高,可以懶加載
3碗旅、DCL + volatile: 線程安全渡处,調(diào)用效率高,可以懶加載
4祟辟、靜態(tài)內(nèi)部類: 線程安全医瘫,調(diào)用效率高,可以懶加載旧困,但不能動態(tài)傳遞參數(shù)
5醇份、枚舉: 線程安全,調(diào)用效率高吼具,但不能延遲加載僚纷,天然地防止反射和反序列化破壞單例
相關(guān)深入文章:《我向面試官講解了單例模式,他對我豎起了大拇指》
不使用 synchronized 關(guān)鍵字拗盒,如何實現(xiàn)線程安全的單例怖竭?
常規(guī)的單例模式都顯式或隱式使用了 synchronized 關(guān)鍵字,懶漢式和 DCL 直接使用了陡蝇,而餓漢式和靜態(tài)內(nèi)部類使用了靜態(tài)成員變量痊臭,其原理其實也是使用了 synchronized。具體要從 Javac 編譯講起登夫,Javac 會將靜態(tài)內(nèi)部類和靜態(tài)代碼塊會整合為 <clinit> 方法广匙,這個方法就是類加載階段的最后一個階段 - 初始化階段。JVM 內(nèi)部會保證多線程環(huán)境下只有一個線程會執(zhí)行 <clinit>恼策,而其它線程需要等待鸦致。最后還有枚舉,枚舉底層是依賴 Enum 類戏蔑,每個枚舉對象其實都是類的靜態(tài)成員蹋凝,本質(zhì)上也和餓漢式類似。
那么总棵,有沒有辦法不使用傳統(tǒng)的互斥機(jī)制實現(xiàn)單例呢鳍寂?有的,可以使用 CAS 或 ThreadLocal情龄,這兩種方法都沒有使用互斥機(jī)制迄汛,但也可以保證線程安全捍壤,優(yōu)點是可以避免線程阻塞和喚醒的上下文切換消耗,也各有缺點:CAS 在資源競爭緊張的情況下鞍爱,會創(chuàng)建大量對象鹃觉,長時間自旋 CAS 也會增大 CPU 負(fù)載。ThreadLocal 的原理是在每個線程都提供一個副本睹逃,其實是以空間換時間盗扇,對內(nèi)存消耗大。
相關(guān)深入文章:
《面試官真是搞笑沉填!讓實現(xiàn)線程安全的單例疗隶,又不讓使用 synchronized!》
《Java 虛擬機(jī) | CAS 比較并交換》
如何破壞單例翼闹?
破壞單例其實就是在單例對象之外再創(chuàng)建另一個對象斑鼻,常規(guī)的對象創(chuàng)建是使用 new 關(guān)鍵字,此外還可以使用反射或反序列化創(chuàng)建對象猎荠,這些方法都可以破壞對象的單例性坚弱。需要注意的是,枚舉天然地可以防止反射和反序列化:使用反射創(chuàng)建枚舉對象時关摇,JDK 會判斷該類是否是一個枚舉類荒叶,如果是會拋出異常;在序列化和反序列化枚舉時拒垃,寫入和讀取的只是枚舉類型和枚舉對象的名字停撞,反序列化時直接使用 枚舉 valueOf(name) 直接查找枚舉對象,不會創(chuàng)建新的對象悼瓮。因此枚舉天然地可以防止破壞單例戈毒。
Kotlin 如何實現(xiàn)單例模式?
相關(guān)深入文章:《Kotlin 下的 5 種單例模式》
Java 創(chuàng)建對象有幾種方式横堡?
創(chuàng)建對象 | 是否調(diào)用構(gòu)造方法 |
---|---|
new 關(guān)鍵字 | 調(diào)用構(gòu)造函數(shù) |
反射(Class#newInstance() & Constructor#newInstance()) | 調(diào)用構(gòu)造函數(shù) |
Object#clone() | 沒有調(diào)用構(gòu)造函數(shù) |
反序列化 | 沒有調(diào)用構(gòu)造函數(shù) |
Java 創(chuàng)建對象可以使用 new埋市、反射、clone() 和反序列化命贴,需要注意道宅,使用 clone() 和反序列化創(chuàng)建對象是不會調(diào)用構(gòu)造函數(shù)的。
new 關(guān)鍵字: 先在堆中創(chuàng)建對象胸蛛,并把對象的引用入棧污茵,隨后 invokespecial 調(diào)用 <init> 方法
Object obj = new Object();
new // class java/lang/Object
dup
invokespecial // Method java/lang/Object.<init> :()V
astore_1
return
dup(完整單詞:duplicate 復(fù)制)會復(fù)制棧上最后一個元素,然后再次入棧葬项。因為 invokespecial 會消耗棧頂?shù)膶ο笠门⒌保匀绻覀兿M{(diào)用 invokespecial 后操作數(shù)棧頂還維持一個指向新建對象的引用,就必須先使用 dup 復(fù)制一份引用民珍。
astore_1 將操作數(shù)棧頂元素出棧并存儲在第 1 位局部變量表襟士。
反射(Class#newInstance() & Constructor#newInstance())
User user = User.class.newInstance();
ldc // class com/User
invokevirtual // Method java/lang/Class.newInstance: ()Ljava/lang/Object;
checkcast // class com/User
astore_1
return
ldc:將常量池引用入棧
checkcast:檢查類型轉(zhuǎn)換合法
Object#clone(): 前提是類實現(xiàn) Cloneable 接口盗飒,復(fù)制對象時,首先創(chuàng)建一個和源對象相同大小的空間陋桂,然后進(jìn)行屬性復(fù)制逆趣,整個過程沒有經(jīng)過構(gòu)造方法。屬性復(fù)制是淺拷貝嗜历,基本類型的成員變量拷貝的是值宣渗,而引用類型的成員變量拷貝的是對象的內(nèi)存地址,不會創(chuàng)建新的對象秸脱。要實現(xiàn)深拷貝落包,需要每個成員變量的對象都實現(xiàn) Cloneabe 并重寫 clone() 方法,進(jìn)而實現(xiàn)對象的層層拷貝摊唇。深拷貝比淺拷貝性能損耗更大。
反序列化
相關(guān)深入文章:《盤點 Java 創(chuàng)建對象的 x 操作》
new 創(chuàng)建對象的過程
Java new 一個對象的過程涯鲁,基本上可以分為 5 個步驟:檢查加載 -> 分配內(nèi)存 -> 初始化零值 -> 設(shè)置對象頭 -> 執(zhí)行 <init> 構(gòu)造函數(shù):
- 1巷查、檢查加載 & 類加載: 檢查類是否被類加載器加載,如果沒有需要先執(zhí)行類加載過程(加載 & 解析 & 初始化)抹腿;
- 2岛请、分配內(nèi)存: Java 對象需要一塊連續(xù)的堆內(nèi)存空間,分配方式有 指針碰撞 & 空閑列表警绩。指針碰撞法要求 Java 堆是絕對規(guī)整的崇败,而空閑列表法不要求 Java 堆是絕對規(guī)整的。由于 Java 堆是線程共享的肩祥,所以需要考慮多線程并發(fā)分配內(nèi)存的問題后室,解決方法有 CAS 操作和 TLAB 分配緩沖。
- 3混狠、初始化零值: 將實例數(shù)據(jù)的值初始化為零值岸霹;
- 4、設(shè)置對象頭: 設(shè)置對象頭信息将饺,包括 Mark Work & 類型指針 & 數(shù)組長度贡避;
- 5、執(zhí)行 <init> 構(gòu)造函數(shù): 執(zhí)行 <init> 構(gòu)造函數(shù)予弧,<init> 由編譯器生成刮吧,包括成員變量初始值、實例代碼塊和對象構(gòu)造函數(shù)掖蛤。
相關(guān)深入文章:Java 虛擬機(jī) | 拿放大鏡看對象
枚舉的實現(xiàn)原理杀捻?
序列化 & 反序列化的原理?
Kotlin lazy 的原理:
final坠七、finally 和 finalize() 有什么區(qū)別水醋?
反射
- 反射可以修改 final 變量嗎旗笔?
5、集合
為什么 HashMap 是線程不安全的拄踪,體現(xiàn)在哪里蝇恶?
- 數(shù)據(jù)覆蓋問題:如果兩個線程并發(fā)執(zhí)行 put 操作,并且兩個數(shù)據(jù)的 hash 值沖突惶桐,就可能出現(xiàn)數(shù)據(jù)覆蓋(線程 A 判斷 hash 值位置為 null撮弧,還未寫入數(shù)據(jù)時掛起,此時線程 B 正常插入數(shù)據(jù)姚糊。接著線程 A 獲得時間片贿衍,由于線程 A 不會重新判斷該位置是否為空,就會把剛才線程 B 寫入的數(shù)據(jù)覆蓋掉)救恨;
- 環(huán)形鏈表問題: 如果兩個線程并發(fā)執(zhí)行 put 操作贸辈,并且觸發(fā)擴(kuò)容,就可能出現(xiàn)環(huán)形鏈表肠槽,此時獲取數(shù)據(jù)會死循環(huán)擎淤。這是因為 JDK 1.7 版本采用頭插法,在擴(kuò)容時會翻轉(zhuǎn)鏈表的順序秸仙,而 JDK 1.8 采用尾插法嘴拢,再擴(kuò)容時會保持鏈表原本的順序,就不會出現(xiàn)鏈表成環(huán)問題了寂纪。
相關(guān)深入文章:都說 HashMap 是線程不安全的席吴,到底體現(xiàn)在哪兒?