在 Java 鍵值對(Key-Value)集合中,HashMap 是使用頻率最高的實現類之一,憑借高效的查找、插入性能,成為日常開發的 “利器”。本文將從 HashMap 的底層原理、核心特點、常用方法到遍歷方式、使用注意事項,進行系統性梳理,幫助快速掌握其核心邏輯與實戰技巧。
一、HashMap 核心認知:底層原理與特點
HashMap 本質是哈希表(數組 + 鏈表 / 紅黑樹)?實現的鍵值對存儲結構,核心目標是通過 “哈希算法” 快速定位元素,平衡查詢與增刪效率。其核心特點如下:
1. 底層存儲結構(JDK 1.8+)
- 基礎結構:數組(稱為 “哈希桶”)+ 鏈表 + 紅黑樹。
- 數組:每個元素是一個 “鏈表頭節點”,通過?
key
?的哈希值計算數組索引(index = (數組長度 - 1) & 哈希值
),實現快速定位。 - 鏈表:當多個?
key
?計算出相同索引(哈希沖突)時,元素以鏈表形式存儲在對應桶中。 - 紅黑樹:當鏈表長度超過?8?且數組長度 ≥ 64 時,鏈表會轉為紅黑樹,將查詢時間復雜度從 O (n) 優化為 O (log n)(避免鏈表過長導致性能下降)。
- 數組:每個元素是一個 “鏈表頭節點”,通過?
- 示意圖簡化理解:
數組索引 0:[Node(key1, val1)] → [Node(key2, val2)] // 鏈表(長度<8)
數組索引 1:[TreeNode(key3, val3)] → 紅黑樹結構 // 紅黑樹(長度≥8)
數組索引 2:null
...
2. 核心特點
- 鍵值對規則:
- Key 唯一:若添加重復 Key,新 Value 會覆蓋舊 Value(
put()
?方法返回舊 Value)。 - Value 可重復:不同 Key 可對應相同 Value。
- 允許 null:Key 最多允許 1 個 null(重復添加 null Key 會覆蓋),Value 可多個 null。
- Key 唯一:若添加重復 Key,新 Value 會覆蓋舊 Value(
- 無序性:存儲順序與插入順序無關(底層按哈希值排序,非插入順序)。
- 線程不安全:非同步設計,多線程同時讀寫可能出現數據異常(如?
ConcurrentModificationException
),需手動處理線程安全(如?Collections.synchronizedMap()
?或?ConcurrentHashMap
)。 - 自動擴容:
- 默認初始容量:16(數組長度,必須是 2 的冪,確保哈希計算均勻)。
- 負載因子:默認 0.75(當元素數量 ≥ 容量 × 負載因子時觸發擴容)。
- 擴容規則:新容量 = 舊容量 × 2,同時重新計算所有元素的哈希索引(“rehash”),會消耗一定性能。
- 高效性能:
- 理想情況下,插入、查詢、刪除的時間復雜度均為?O(1)(直接通過哈希值定位桶)。
- 哈希沖突較少時,性能接近理想值;沖突嚴重(鏈表過長)時,性能會下降(紅黑樹優化可緩解此問題)。
二、HashMap 常用方法
HashMap 提供了豐富的方法操作鍵值對,以下是開發中最常用的方法,均附完整示例代碼,可直接復制運行。
1. 基礎操作:添加、獲取、刪除
(1)添加鍵值對(put ())
V put(K key, V value)
:添加鍵值對,若 Key 已存在則覆蓋 Value,返回舊 Value(若 Key 不存在則返回 null)。void putAll(Map<? extends K, ? extends V> m)
:將另一個同類型 Map 的所有鍵值對添加到當前 HashMap 中(重復 Key 會被覆蓋)。
import java.util.HashMap;public class HashMapPutDemo {public static void main(String[] args) {// 1. 單個鍵值對添加HashMap<String, Integer> scoreMap = new HashMap<>();Integer oldMathScore = scoreMap.put("數學", 90); // Key 不存在,返回 nullSystem.out.println("舊數學成績:" + oldMathScore); // 輸出:nullscoreMap.put("語文", 85);scoreMap.put("英語", 95);System.out.println("添加后:" + scoreMap); // 輸出:{數學=90, 語文=85, 英語=95}// 重復 Key 覆蓋:數學成績從 90 改為 98Integer updatedOldScore = scoreMap.put("數學", 98);System.out.println("被覆蓋的舊數學成績:" + updatedOldScore); // 輸出:90System.out.println("覆蓋后:" + scoreMap); // 輸出:{數學=98, 語文=85, 英語=95}// 2. 批量添加(putAll())HashMap<String, Integer> extraScoreMap = new HashMap<>();extraScoreMap.put("物理", 88);extraScoreMap.put("化學", 92);scoreMap.putAll(extraScoreMap);System.out.println("批量添加后:" + scoreMap); // 輸出:{數學=98, 語文=85, 英語=95, 物理=88, 化學=92}}
}
(2)獲取值與判斷存在(get ()、containsKey ()、containsValue ())
V get(Object key)
:根據 Key 獲取 Value,若 Key 不存在則返回?null(注意:若 Value 本身是 null,需用?containsKey()
?區分 “Key 不存在” 和 “Value 為 null”)。boolean containsKey(Object key)
:判斷 HashMap 是否包含指定 Key,返回布爾值。boolean containsValue(Object value)
:判斷 HashMap 是否包含指定 Value,返回布爾值。
public class HashMapGetContainsDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("語文", 85);scoreMap.put("生物", null); // Value 為 null// 1. 根據 Key 獲取 ValueInteger mathScore = scoreMap.get("數學");Integer historyScore = scoreMap.get("歷史"); // Key 不存在Integer bioScore = scoreMap.get("生物"); // Value 本身是 nullSystem.out.println("數學成績:" + mathScore); // 輸出:98System.out.println("歷史成績(Key 不存在):" + historyScore); // 輸出:nullSystem.out.println("生物成績(Value 為 null):" + bioScore); // 輸出:null// 2. 判斷 Key 是否存在(區分“Key 不存在”和“Value 為 null”)boolean hasBioKey = scoreMap.containsKey("生物");boolean hasHistoryKey = scoreMap.containsKey("歷史");System.out.println("是否包含 Key '生物':" + hasBioKey); // 輸出:trueSystem.out.println("是否包含 Key '歷史':" + hasHistoryKey); // 輸出:false// 3. 判斷 Value 是否存在boolean has98 = scoreMap.containsValue(98);boolean has100 = scoreMap.containsValue(100);System.out.println("是否包含 Value 98:" + has98); // 輸出:trueSystem.out.println("是否包含 Value 100:" + has100); // 輸出:false}
}
(3)刪除鍵值對(remove ())
V remove(Object key)
:根據 Key 刪除鍵值對,返回被刪除的 Value(若 Key 不存在則返回 null)。
public class HashMapRemoveDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("語文", 85);scoreMap.put("英語", 95);// 刪除 Key 為“語文”的鍵值對Integer removedChineseScore = scoreMap.remove("語文");System.out.println("被刪除的語文成績:" + removedChineseScore); // 輸出:85System.out.println("刪除后:" + scoreMap); // 輸出:{數學=98, 英語=95}// 刪除不存在的 KeyInteger removedHistoryScore = scoreMap.remove("歷史");System.out.println("刪除不存在的 Key 返回值:" + removedHistoryScore); // 輸出:null}
}
2. 進階操作:修改、清空、判斷空否
(1)修改 Value(replace ())
V replace(K key, V value)
:僅當 Key 存在時,用新 Value 替換舊 Value,返回舊 Value(若 Key 不存在則返回 null,區別于?put()
:put()
?會新增不存在的 Key)。
public class HashMapReplaceDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("英語", 95);// 修改存在的 Key(英語成績從 95 改為 97)Integer oldEnglishScore = scoreMap.replace("英語", 97);System.out.println("被修改的舊英語成績:" + oldEnglishScore); // 輸出:95System.out.println("修改后:" + scoreMap); // 輸出:{數學=98, 英語=97}// 修改不存在的 Key(不會新增,返回 null)Integer oldHistoryScore = scoreMap.replace("歷史", 80);System.out.println("修改不存在的 Key 返回值:" + oldHistoryScore); // 輸出:nullSystem.out.println("修改后集合:" + scoreMap); // 輸出:{數學=98, 英語=97}(無變化)}
}
(2)清空與判斷空否(clear ()、isEmpty ()、size ())
void clear()
:清空 HashMap 中所有鍵值對(集合變為空,對象本身仍存在)。boolean isEmpty()
:判斷 HashMap 是否為空(元素個數為 0),返回布爾值。int size()
:返回 HashMap 中鍵值對的實際個數(區別于 “容量”)。
public class HashMapClearEmptySizeDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("英語", 97);// 1. 獲取集合大小System.out.println("初始元素個數:" + scoreMap.size()); // 輸出:2// 2. 判斷是否為空System.out.println("初始是否為空:" + scoreMap.isEmpty()); // 輸出:false// 3. 清空集合scoreMap.clear();System.out.println("清空后元素個數:" + scoreMap.size()); // 輸出:0System.out.println("清空后是否為空:" + scoreMap.isEmpty()); // 輸出:true}
}
3. HashMap 三種核心遍歷方式
HashMap 存儲的是 “鍵值對(Entry)”,遍歷需圍繞 “Key 集合”“Value 集合”“Entry 集合” 展開,三種常用方式如下:
(1)遍歷 Key 集合,再獲取 Value(keySet ())
通過?keySet()
?獲取所有 Key 的集合,遍歷 Key 后用?get(key)
?獲取對應 Value,適合僅需 Key 或需通過 Key 處理 Value 的場景。
public class HashMapKeySetDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("語文", 85);scoreMap.put("英語", 97);// 遍歷 Key 集合for (String subject : scoreMap.keySet()) {Integer score = scoreMap.get(subject);System.out.println(subject + ":" + score);}// 輸出(順序不固定):// 數學:98// 語文:85// 英語:97}
}
(2)直接遍歷 Entry 集合(entrySet ())
通過?entrySet()
?獲取所有鍵值對(Map.Entry<K, V>
)的集合,直接獲取 Key 和 Value,效率最高(無需二次?get(key)
?查詢),是開發首選。
public class HashMapEntrySetDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("語文", 85);scoreMap.put("英語", 97);// 遍歷 Entry 集合(推薦)for (HashMap.Entry<String, Integer> entry : scoreMap.entrySet()) {String subject = entry.getKey(); // 獲取 KeyInteger score = entry.getValue(); // 獲取 ValueSystem.out.println(subject + ":" + score);}// 輸出(順序不固定):// 數學:98// 語文:85// 英語:97}
}
(3)遍歷 Value 集合(values ())
通過?values()
?獲取所有 Value 的集合,僅遍歷 Value,適合無需 Key、僅需處理 Value 的場景(無法通過 Value 反向獲取 Key)。
public class HashMapValuesDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("數學", 98);scoreMap.put("語文", 85);scoreMap.put("英語", 97);// 遍歷 Value 集合System.out.println("所有成績:");for (Integer score : scoreMap.values()) {System.out.println(score);}// 輸出(順序不固定):// 98// 85// 97}
}
三、HashMap 使用注意事項
Key 的選擇原則:
- Key 必須重寫?
hashCode()
?和?equals()
?方法(否則無法正確判斷 Key 唯一性,導致哈希沖突無法解決)。 - 推薦使用不可變類作為 Key(如?
String
、Integer
):若 Key 是可變對象,修改后哈希值變化,會導致無法通過原 Key 獲取 Value。 - 示例:若用?
User
?類作為 Key,需手動重寫方法:
- Key 必須重寫?
class User {private String id;// 重寫 hashCode() 和 equals()@Overridepublic int hashCode() { return id.hashCode(); }@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;User user = (User) o;return Objects.equals(id, user.id);}
}
null 鍵值的注意事項:
- Key 最多 1 個 null,重復添加會覆蓋;Value 可多個 null。
- 用?
get(key)
?獲取 Value 時,若返回 null,需通過?containsKey(key)
?確認是 “Key 不存在” 還是 “Value 為 null”。
線程安全問題:
- 單線程環境:直接使用 HashMap 即可。
- 多線程環境:
- 若需弱一致性:使用?
Collections.synchronizedMap(new HashMap<>())
(對整個 HashMap 加鎖,性能較低)。 - 若需高性能:優先使用?
ConcurrentHashMap
(JDK 1.8+ 采用分段鎖,性能優于同步 HashMap)。
- 若需弱一致性:使用?
性能優化技巧:
- 初始容量指定:若已知元素數量,創建時指定初始容量(如?
new HashMap<>(100)
),避免頻繁擴容(擴容需 rehash,消耗性能)。 - 負載因子調整:默認 0.75 是 “性能與空間” 的平衡,若內存充足可降低(如 0.5,減少哈希沖突),若內存緊張可提高(如 0.8,減少數組占用空間)。
- 避免哈希沖突:合理重寫 Key 的?
hashCode()
?方法,盡量讓哈希值均勻分布,減少鏈表 / 紅黑樹的長度。
- 初始容量指定:若已知元素數量,創建時指定初始容量(如?
與 TreeMap/Hashtable 的區別(避免混淆):
特性 | HashMap | TreeMap | Hashtable(不推薦) |
---|---|---|---|
排序 | 無序(按哈希值) | 有序(Key 自然排序 / 自定義排序) | 無序 |
線程安全 | 非線程安全 | 非線程安全 | 線程安全(全方法同步) |
null 允許 | Key 1 個 null,Value 多個 null | 不允許 null | 不允許 null |
底層結構 | 數組 + 鏈表 / 紅黑樹 | 紅黑樹 | 數組 + 鏈表 |
適用場景 | 通用高效查詢 | 需要有序鍵值對 | 遺留多線程場景(已被 ConcurrentHashMap 替代) |
四、總結
HashMap 是 Java 鍵值對集合的核心實現,核心優勢在于 “哈希表” 帶來的 O (1) 高效性能,適合大多數無需有序、單線程的鍵值對存儲場景。掌握其底層結構(數組 + 鏈表 / 紅黑樹)、常用方法(put/get/remove/ 遍歷)及使用注意事項(Key 重寫方法、線程安全、性能優化),就能在開發中靈活應對各類場景。