目錄
1、Java語言相關
1.1、String的intern方法
1.2、HashMap的擴容
1.3、Java數組不支持泛型
1.4、泛型類型保留到運行時
1.5、匿名內部類使用的外部變量需要加final
2、Kotlin語言相關
3、設計模式
1、Java語言相關
1.1、String的intern方法
1)、String的存儲機制
String內存模型的核心特點包括:
- 不可變性:String實例一旦創建不可修改
- 雙重存儲機制:實例可能存儲在堆或字符串常量池
- intern方法作用:將字符串顯式存入字符串池中
String類的特殊性體現在:
- 不可變性:與基本數據類型封裝類相同
- 聲明語法特殊:唯一可通過字面量創建的引用類型
- 存儲位置特殊:實例可能存在于字符串常量池而非堆
①、String不可變性的實現機制:
- final類聲明:禁止繼承
- private final char[]存儲數據:外部無法修改
- 方法設計原則:比如substring等操作均返回新實例
②、String實例不可變的表現:
- 任何修改操作(如拼接、截取)均生成新實例
- 原實例內容始終保持不變
- 必須通過改變引用指向新實例實現"修改"效果
2)、字符串常量池
- 字符串池與常量池的區別:常量池存在于每個class文件的邏輯結構中,存儲以字面量方式聲明的字符串;運行時常量池是class文件常量池加載到內存后的形態。
- 字符串池功能:全局存儲字符串(非每個類獨立),HotSpot虛擬機通過哈希表實現。
- 關聯性:常量池中的string類型常量通過查詢字符串池解析,二者通過引用保持一致性,導致概念易混淆。
字符串常量池的優化原理:
- 相同字面量的多個String實例可共享
- intern方法實現實例復用
- 哈希值相同且equals為true的實例可合并
內存優化示例:字面量均為"ABC"的name1和name2引用可通過intern指向同一實例。
3)、總結
string的不可變性使字符串池復用成為可能,JDK1.7+的字符串池存儲堆引用,相關面試題多圍繞內存模型展開,需理解存儲機制。
- JDK 1.7以下(不包括1.7),字符串池在永久代,intern方法的含義是:
- 如果字符串池中存在與當前字符串內容相同的緩存,則返回緩存實例;
- 否則,在字符串池中創建相應實例,并返回。
- JDK1.7及以上,字符串池在堆中,字符串池中,存儲的是字符串堆引用,intern方法的含義是:
- 如果字符串池中存在與當前字符串內容相同的緩存,則返回緩存的引用;
- 否則,在字符串池中存儲當前字符串的引用。
1.2、HashMap的擴容
HashMap容量選擇二的N次方的原因如下:
- 計算數組下標時可通過位運算替代取余操作,從而提升運算性能
- 擴容時無需重新計算所有節點位置,僅需檢查新參與運算的最高位即可
1)哈希算法
散列算法將任意長度輸入轉換為固定長度輸出,要求相同輸入必產生相同輸出。哈希碰撞指不同輸入產生相同輸出的情況。理想哈希算法應最大限度避免碰撞,簡單取余算法因碰撞概率高不適用于高性能場景。
哈希碰撞:哈希表中元素位置通過哈希碼對容量取余確定。碰撞發生時元素會被放入相同桶中,該機制是哈希表定位元素的基礎算法。
2)HashMap
HashMap主體結構是初始長度16的數組,數組存儲Node類型元素。傳統桶結構采用鏈表實現,JDK1.8新增紅黑樹實現(TreeNode為Node子類)。數組與紅黑樹會根據元素數量動態轉換,紅黑樹在數據量大時具有性能優勢。
①、put方法
put方法實際調用putVal實現存儲,過程中會通過hash()方法重新計算鍵的哈希碼。
②、putVal方法
putVal方法執行流程如下:
- 檢測數組是否為空,空則觸發resize擴容
- 計算元素數組下標(非直接取余)
- 下標為空時新建節點插入
- 下標非空時判斷鍵是否相等,相等則覆蓋值
- 不相等時根據節點類型(鏈表/紅黑樹)遍歷查找
- 鏈表插入后檢查是否需轉為紅黑樹
- 最終檢查是否觸發擴容
計算元素在數組中的下標:下標計算采用(n-1)&hash替代取余操作,二者數學等價但位運算效率更高。以容量16為例:
- 16-1=15(二進制1111)
- 與哈希碼按位與操作可保留后四位
- 該優化的前提是容量必須為2的n次冪,此為設計原因之一。
③、hash方法
hash()方法通過擾動函數解決低位碰撞問題:
- 將哈希碼無符號右移16位使高位參與運算
- 與原值異或融合高位特征到低位
- JDK 1.8的擾動邏輯較1.7簡化但原理一致,核心目標是提升低位隨機性。
④、resize方法
擴容機制特性分析:
維度 | JDK 1.7前 | JDK 1.8優化 |
容量變化 | 翻倍為2^n | 保持2^n特性 |
重定位方式 | 全量重新計算下標 | 僅檢查新增最高位 |
元素移動 | 全部遷移 | 原位置或原位置+舊容量 |
優化依據:擴容后元素新位置僅取決于新增運算位(0保持原位,1偏移舊容量值)。此特性同樣依賴2^n容量設計。 |
1.3、Java數組不支持泛型
Java數組不支持泛型的說法需明確兩點:泛型僅在編譯期提供有限類型安全保證,且實際限制在于無法創建泛型類型數組。核心原因包括:
- 類型擦除機制使運行時泛型信息丟失
- 數組協變性與泛型設計理念沖突
- Java泛型實現受歷史兼容性制約
Java數組不支持泛型的原因在于數組與泛型在設計理念上存在根本性沖突。數組是JVM底層實現的特殊實例類型,自Java早期版本就已存在;而泛型在Java 1.5版本才引入。兩者核心矛盾在于:數組具有協變性(父類型數組引用可指向子類型數組實例),而泛型默認具有不變性(泛型類之間無繼承關系)。這種設計差異導致無法通過編譯期類型安全檢查創建泛型數組實例。
1)、Java泛型的實現方式
類型擦除機制特點:
- 泛型類型參數僅在編譯期有效,字節碼中替換為原始類型
- 生成橋接方法保證多態性
- 歷史原因:保持與Java1.5前版本的二進制兼容性
2)、泛型的類型擦除機制
泛型類型擦除機制實現方式可通過反編譯字節碼觀察。new指令及構造方法調用均不包含泛型信息,創建的ArrayList實際為Object類型。繞過編譯器報錯可將任意類型元素放入ArrayList,此現象解釋了字符串元素存入非泛型集合的可能性。
類型擦除:類定義的泛型經類型擦除后固化為Object。子類繼承父類時給定的泛型參數需具體化,例如實現change方法時參數與返回值被具體化為Apple類型。由于父類方法簽名中為Object,編譯器自動生成橋接方法以維持重寫關系。類型擦除導致運行時無法創建泛型數組,且泛型無法在運行時保證內存安全。總結:
- 字節碼創建數組需明確類型
- 類型擦除使泛型無法延續至運行時
- Java泛型無法實現運行時類型安全檢查
3)、Kotlin中的數組
Kotlin數組支持泛型通過兩種實現方式:
數組類型 | 實現特性 | 對應字節碼指令 | Java等效類型 |
類集合語法數組 | 泛型實現類似List,不具備協變性 | anewarray | 包裝類類型數組 |
基本類型數組 | 兼容Java基本數據類型 | newarray | 基本數據類型數組 |
Kotlin通過IntArray等類封裝基本類型數組,反編譯顯示其字節碼同樣不包含泛型信息,證實Kotlin泛型基于類型擦除機制實現。
4)、Kotlin為何支持創建泛型數組
Kotlin雖采用類型擦除機制,但通過強制泛型類型聲明消除安全隱患。由于無需兼容歷史字節碼或源碼,集合與數組API設計均要求顯式指定泛型類型,此設計規避了Java中存在的類型安全問題。
1.4、泛型類型保留到運行時
1)、使用Class<T>
<T> T create(Class<T> kind) {
return new kind.newInstance();
} // TODO catch
通過Class<T>參數傳遞類型信息。該方案特點:
- 支持反射創建實例
- 局限性:
- 反射效率低
- 無法處理嵌套泛型(如List<String>.class非法)
2)、通過子類把泛型類型具體化(reified)
public class AppleBasket extends Basket<Apple>{
... // 這里Apple具體化
}
public class GenericsTypeToken<T> {
public Type type = null;
public GenericsTypeToken() {
type =
((ParameterizedType)this.getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
}
}
new GenericsTypeToken<Basket<Apple>>() {}.type;
字節碼簽名保留泛型信息。實現步驟:
- 繼承具體化泛型類
- 通過getGenericSuperclass()獲取簽名屬性 - 解析Type接口獲取實際類型
- Gson等庫采用類似TypeToken機制實現泛型反序列化。
Type type = new TypeToken<List<User>>() {}.getType();
List<User> list = new Gson().fromJson(userJson, userListType);
1.5、匿名內部類使用的外部變量需要加final
- 匿名內部類, 持有外部類的引用;
- 以編譯器自動生成的成員變量的形式持有;
- 通過編譯器自動生成的構造方法傳入。
- 匿名內部類, 通過這個引用訪問外部類的成員變量和方法。
- 匿名內部類, 訪問外部局部變量時, 其實是訪問自身的一個成員變量;
- 這個成員變量, 是編譯器自動生成的(val$變量);
- 這個成員變量, 由編譯器自動生成的構造方法初始化;
- 為了保證這個成員變量和外部局部變量時刻保持一致性, 二者必須都是final的。
2、Kotlin語言相關
推薦閱讀:一文帶你快速掌握Kotlin核心技能
3、設計模式
設計模式:是前人通過試錯總結的最佳實踐,用于解決特定問題(如提高代碼復用性、可讀性、健壯性)。那我們遇到過哪些設計模式的使用案例呢?
①、模板方法模式
- Android Activity生命周期:開發者通過重寫onCreate、onResume等方法響應狀態變更,無法干預狀態流轉邏輯。
- 核心要點:
- 父類定義算法框架,子類實現細節。
- 決策權在高層模塊(如父類控制調用時機)。
- 與策略模式區別:模板方法通過繼承封裝算法,策略模式通過組合封裝。
②、責任鏈模式
責任鏈模式通過鏈式節點處理請求,經典案例有:
- Android View事件分發:事件沿視圖層級傳遞直至被消費。
- OkHttp攔截器:請求通過攔截器鏈逐層處理。
責任鏈模式適用于多個處理者具備處理相同事件能力且需篩選最符合要求或優先級最高的場景。核心特征包括:
- 職責差異化:責任鏈各節點具備不同職責
- 順序依賴性:節點間存在明確先后順序
- 擴展優勢:通過接口實現新責任并加入責任鏈即可完成功能擴展
- 動態調整:支持運行時修改責任鏈節點構成
③、裝飾者模式
裝飾者模式的核心在于動態擴展類的能力,通過組合而非繼承實現功能擴展。
安卓平臺的ContextWrapper類采用裝飾者模式。通過包裝ContextImpl實現動態功能擴展,避免直接繼承帶來的局限性。
④、代理模式
代理模式與裝飾者模式結構相似,均實現接口并持有接口引用。代理模式通過替身控制對象訪問,代理模式的要點包括遠程對象代理和虛擬代理。與裝飾者模式目的不同,前者控制訪問,后者擴展行為。
典型場景:安卓框架層通過Binder跨進程實現代理模式。ActivityManager通過IActivityManager接口代理跨進程訪問。
⑤、Android中的其它設計模式
安卓平臺常見設計模式包括:
- 觀察者模式:廣播、Adapter的notifyDataSetChanged
- 適配器模式:ListView的Adapter
- 構建者模式:AlertDialog.Builder
- 備忘模式:Activity的onSaveInstanceState
- 迭代器模式:Cursor遍歷
- 原型模式:Intent的clone方法
- 享元模式:線程池、連接池
- 命令模式:Handler消息循環
OK,今天的內容就這么多了,下期再會!