一、設計原則高頻面試題(附大廠真題解析)
1. 單一職責原則(SRP)在 Android 開發中的應用(字節跳動真題)
- 真題:“你在項目中如何體現單一職責原則?舉例說明。”
- 考點:結合實際場景說明職責拆分,避免貧血模型。
- 滿分答案:
在開發網絡模塊時,原代碼將 “網絡請求邏輯”“數據解析”“錯誤處理” 耦合在一個NetworkManager
類中,違反 SRP。
重構方案:- 拆分為
OkHttpHelper
(負責底層網絡請求)、DataParser
(解析 JSON/Proto 數據)、ErrorHandler
(統一處理網絡異常); - 高層模塊(如
UserRepository
)依賴這三個抽象組件,通過構造器注入協作。
優勢:每個類僅一個修改原因(如網絡庫升級只需改OkHttpHelper
),降低維護成本。
- 拆分為
2. 里氏替換原則(LSP)的經典反例及修正(騰訊真題)
- 真題:“為什么 Square 不能直接繼承 Rectangle?如何正確設計?”
- 考點:理解繼承的約束條件,區分 “Is-a” 與 “Can-do”。
- 滿分答案:
反例分析:Rectangle
定義setWidth(int w)
和setHeight(int h)
,允許寬高獨立變化;Square
重寫這兩個方法時,強制寬高相等,破壞父類 “寬高可獨立設置” 的契約,違反 LSP(子類不能替換父類)。
修正方案:
- 放棄繼承,讓
Square
和Rectangle
實現共同接口Quadrilateral
,提供getWidth()
和getHeight()
,但不強制寬高可獨立設置; - 或引入
MutableRectangle
接口,明確 “可修改寬高” 的能力,Square
不實現該接口。
3. 依賴倒置原則(DIP)在 MVP 中的應用(阿里真題)
- 真題:“MVP 架構如何體現依賴倒置原則?”
- 考點:區分高層模塊與低層模塊,抽象接口解耦。
- 滿分答案:
MVP 分層:- 高層模塊(Presenter):依賴抽象接口
View
(如UserView
)和Repository
(如UserRepository
),而非具體實現(Activity
或RetrofitImpl
); - 低層模塊(Model/View 實現):實現這些抽象接口,如
UserActivity implements UserView
,UserRetrofit implements UserRepository
。
依賴關系:
Presenter 與具體 Activity / 網絡庫解耦,可通過依賴注入(如 Dagger)切換實現(如單元測試時用 MockView),符合 “高層模塊依賴抽象” 的 DIP 原則。
- 高層模塊(Presenter):依賴抽象接口
二、DCL 單例模式大廠真題解析
1. 為什么 DCL 單例需要 volatile?(美團真題)
- 考點:理解指令重排對單例的影響,volatile 的內存語義。
- 滿分答案:
指令重排風險:
instance = new DCLSingleton()
可分解為:- 分配內存空間(
memory = allocate()
); - 初始化對象(
ctorInstance(memory)
); - 將內存地址賦給
instance
(instance = memory
)。
JVM 可能重排為 1→3→2,若線程 A 執行到 3 時(instance
非空但未初始化),線程 B 調用getInstance()
返回未初始化的對象,導致 NPE。
volatile 作用:
- 禁止指令重排,確保 1→2→3 的順序;
- 保證可見性,線程 A 修改
instance
后,線程 B 立即看到最新值。
JDK 版本關鍵:JDK 1.5 + 修復了 volatile 的語義,此前版本 DCL 可能失效,因此現代 Java 必須使用 volatile。
- 分配內存空間(
2. 單例模式的線程安全實現有哪些?對比優缺點(百度真題)
- 考點:掌握不同單例實現的適用場景,反序列化安全。
- 滿分答案:
實現方式 | 線程安全 | 優點 | 缺點 | 大廠應用場景 |
---|---|---|---|---|
DCL | 是 | 延遲初始化,性能高 | 需 volatile,實現較復雜 | 高并發且內存敏感場景 |
靜態內部類 | 是 | 簡潔,利用類加載機制安全 | 類加載后立即初始化 | 通用場景(推薦) |
枚舉單例 | 是 | 反序列化安全,防止反射攻擊 | 不支持延遲初始化 | 需嚴格防止實例化場景 |
- 反序列化安全:
枚舉單例天然支持(Java 規范保證反序列化返回枚舉常量),其他方式需重寫readResolve()
返回單例實例:protected Object readResolve() { return instance; }
三、HashMap 高頻面試題(附大廠真題解析)
1. JDK8 HashMap 為什么引入紅黑樹?鏈表轉紅黑樹的條件?(字節跳動真題)
-
考點:理解哈希沖突優化,閾值設計原理。
-
滿分答案:
引入紅黑樹原因:
JDK7 及以前用鏈表處理哈希沖突,當鏈表長度為 n 時,查找時間復雜度 O (n)。數據傾斜時(如大量鍵哈希值相同),鏈表可能很長,性能下降。
紅黑樹將查找、插入、刪除的時間復雜度降至 O (logn),提升極端場景下的性能。轉換條件(兩個同時滿足):
- 鏈表長度≥8(
TREEIFY_THRESHOLD=8
); - 數組容量≥64(
MIN_TREEIFY_CAPACITY=64
)。
原因:
- 鏈表長度 8 的概率極低(泊松分布計算,概率僅 0.0000006),若出現則認為是哈希沖突嚴重;
- 若數組容量小(如 16),直接擴容比轉紅黑樹更高效(減少樹節點維護開銷)。
- 鏈表長度≥8(
2. 自定義類作為 HashMap 的 Key 需要注意什么?(阿里真題)
- 考點:正確重寫 hashCode 和 equals,不可變性。
- 滿分答案:
- 必須重寫
hashCode()
和equals()
:- 若只重寫
equals
,不同對象可能哈希值相同,導致存入 HashMap 后無法正確查找; - 示例:
class Person { String id; @Override public boolean equals(Object o) { ... } // 必須同時重寫hashCode,保證相等對象哈希值相同 @Override public int hashCode() { return Objects.hash(id); } }
- 若只重寫
- Key 建議為不可變類:
- 若 Key 可變,修改后哈希值變化,導致存入的鍵值對無法通過新值查找(如
String
是不可變類,推薦作為 Key); - 若必須用可變類,修改前先從 HashMap 中刪除舊 Key。
- 若 Key 可變,修改后哈希值變化,導致存入的鍵值對無法通過新值查找(如
- 必須重寫
四、ConcurrentHashMap 大廠真題解析
1. JDK7 與 JDK8 的 ConcurrentHashMap 實現有何區別?(騰訊真題)
- 考點:分段鎖 vs 細粒度鎖,數據結構演進。
- 滿分答案:
特性 | JDK7(分段鎖) | JDK8(CAS + 細粒度鎖) |
---|---|---|
數據結構 | Segment 數組(每個 Segment 是小 HashMap) | 數組 + 鏈表 + 紅黑樹(同 HashMap 結構) |
鎖機制 | 對 Segment 加 ReentrantLock(鎖粒度大) | 對鏈表頭節點或紅黑樹根節點加 synchronized(鎖粒度小) |
插入邏輯 | 鎖 Segment 后遍歷鏈表 | 先 CAS 無鎖插入,失敗后加鎖 |
并發度 | 受限于 Segment 數量(默認 16) | 理論并發度更高(鎖競爭更小) |
內存效率 | 每個 Segment 有獨立數組,內存占用略高 | 共享數組,內存更緊湊 |
- 典型場景:
JDK8 在高并發寫入場景(如秒殺系統的計數器)性能提升顯著,因鎖粒度從 “段” 細化到 “節點”,減少線程競爭。
2. ConcurrentHashMap 為什么不允許 Key 和 Value 為 null?(美團真題)
- 考點:線程安全與 null 值的歧義性。
- 滿分答案:
歷史原因:- HashMap 允許 null Key(唯一)和 null Value,ConcurrentHashMap 為避免與 Hashtable(不允許 null)行為不一致,選擇不允許 null;
- 更重要的是,null 值在多線程場景下存在歧義:
- 當
get(key)
返回 null 時,無法區分 “Key 不存在” 和 “Value 為 null”; - 若允許 null Value,多線程插入時可能出現 “Key 存在但 Value 為 null” 的中間狀態,導致后續讀取誤判。
對比 HashMap:
HashMap 單線程下可明確處理 null(Key 只能有一個 null,Value 可為多個 null),但 ConcurrentHashMap 作為線程安全類,需避免這種歧義性,保證語義清晰。
- 當
五、面試真題陷阱與避坑指南
1. 設計原則陷阱題:“所有類都應該遵守單一職責原則嗎?”(字節跳動)
- 陷阱:考察對原則的靈活應用,而非教條主義。
- 正確回答:
不是。單一職責原則的 “職責” 是 “變化的原因”,若多個職責不會同時變化(如 “用戶校驗” 和 “日志記錄” 在項目中始終一起修改),可暫時合并以減少類數量。原則需結合項目規模和變化頻率權衡,避免過度設計。
2. HashMap 擴容陷阱:“初始容量設為 10,實際數組長度是多少?”(阿里)
- 陷阱:HashMap 會將容量自動調整為≥給定值的最小 2 的冪(10→16)。
- 正確回答:
實際長度為 16。HashMap 的構造函數會調用tableSizeFor(int cap)
方法,將容量向上取整為 2 的冪,確保(n-1)&hash
的計算正確性。
面試擴展:
1. 設計原則綜合題:“MVC 架構是否符合開閉原則?為什么?”
- 考點:架構與設計原則的結合,擴展性分析。
- 預測答案:
部分符合:- View 層(如 Activity)常因 UI 變化直接修改,違反 “對修改關閉”;
- Model 層(數據模型)和 Controller 層(邏輯處理)可通過抽象接口擴展(如新增數據源時實現新 Model 接口),符合 “對擴展開放”。
改進建議:
引入接口隔離,讓 View 依賴抽象(如 MVP 中的 View 接口),減少對具體實現的修改,更貼近開閉原則。
2. HashMap 深度陷阱題:“鍵的 hashCode () 返回 0,會發生什么?如何優化?”
- 考點:極端哈希沖突處理,紅黑樹閾值。
- 預測答案:
- 所有鍵存入數組的 0 號位置,形成長鏈表(或紅黑樹);
- 若數組容量≥64 且鏈表長度≥8,轉為紅黑樹,查詢時間復雜度 O (logn);
- 優化:重寫 hashCode (),讓鍵的哈希值更分散(如結合多個字段計算哈希)。
六、總結
知識點 | 高頻問題示例 | 核心考點 | 滿分答案關鍵要素 |
---|---|---|---|
單一職責原則 | 如何拆分 Android 中的網絡模塊? | 職責定義(變化原因)、實際案例 | 拆分前后對比,說明每個類的獨立變化原因 |
DCL 單例 | 為什么需要兩次檢查和 volatile? | 線程安全、指令重排、可見性 | 結合源碼解釋兩次檢查的作用,volatile 的必要性 |
HashMap 紅黑樹 | 鏈表轉紅黑樹的條件是什么? | 閾值設計、概率分析、性能權衡 | 同時滿足長度≥8 和容量≥64,避免小樹維護開銷 |
ConcurrentHashMap | 與 Hashtable 的區別? | 鎖機制、null 支持、并發度 | 細粒度鎖 vs 全表鎖,弱一致性設計 |
設計原則與集合類核心考點
├─ 設計原則(6大原則)
│ ├─ SRP:職責=變化原因,拆分類/模塊
│ ├─ OCP:通過抽象擴展,避免修改原有代碼
│ ├─ LSP:子類可替換父類,不破壞契約
│ ├─ ISP:接口細化,客戶端不依賴無用方法
│ ├─ DIP:高層模塊依賴抽象,而非具體實現
│ └─ LoD:僅與直接朋友交互,減少耦合
├─ DCL單例
│ ├─ 雙重檢查+volatile:防指令重排,線程安全
│ ├─ 防御反射/反序列化:構造函數檢查+readResolve
│ └─ 最佳實踐:枚舉單例(最簡、最安全)
├─ HashMap
│ ├─ 底層:數組+鏈表(≥8轉紅黑樹,容量≥64)
│ ├─ 哈希計算:高位異或,減少沖突
│ ├─ 擴容:容量翻倍,重新哈希(初始容量設為2的冪)
│ └─ Key要求:重寫hashCode/equals,不可變類最佳
└─ ConcurrentHashMap ├─ 線程安全:JDK8細粒度synchronized+CAS ├─ 與HashMap區別:不允許null,弱一致性 └─ 適用場景:高并發讀寫,替代Hashtable/同步HashMap