核心概念
HashMap 允许 null key,并且最多只能有 1 个 null key。null key 总是被存储在数组的 第 0 个位置(index = 0)。
源码实现
1. hash 方法对 null 的处理
static final int hash(Object key) {
int h;
// key 为 null 时,hash 值固定为 0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
关键点:
- null key 的 hash 值固定为 0
- 通过
(n-1) & 0计算索引,结果永远是 0 - 因此 null key 总是存储在
table[0]位置
2. put 方法处理 null key
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
// ↑ null key 的 hash 为 0
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// ...
// 计算索引位置
if ((p = tab[i = (n - 1) & hash]) == null)
// ↑ null key: (n-1) & 0 = 0,固定在 index=0
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 检查第一个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// ↑ null key 通过 == 比较
e = p;
// ...
}
// ...
}
3. get 方法处理 null key
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
// ↑ null key 的 hash 为 0
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// ↑ null key: 直接访问 table[0]
// 检查第一个节点
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
// ↑ null key 通过 == 判断
return first;
// 遍历链表或树
if ((e = first.next) != null) {
// ...
}
}
return null;
}
实际使用示例
基本操作
public class NullKeyTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// 1. 插入 null key
map.put(null, "value1");
System.out.println(map.get(null)); // 输出: value1
// 2. 更新 null key 的值
map.put(null, "value2");
System.out.println(map.get(null)); // 输出: value2
System.out.println(map.size()); // 输出: 1(只有一个 null key)
// 3. null key 与普通 key 共存
map.put("key1", "value3");
map.put("key2", "value4");
System.out.println(map.size()); // 输出: 3
// 4. 判断是否包含 null key
System.out.println(map.containsKey(null)); // true
// 5. 删除 null key
map.remove(null);
System.out.println(map.get(null)); // 输出: null
System.out.println(map.size()); // 输出: 2
}
}
null key 与 null value 的组合
public class NullKeyAndValueTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// 1. null key + 非 null value
map.put(null, "value1");
// 2. null key + null value
map.put(null, null);
// 3. 非 null key + null value
map.put("key1", null);
// 4. 区分"不存在"和"值为null"
System.out.println(map.containsKey(null)); // true
System.out.println(map.get(null)); // null
System.out.println(map.containsKey("key1")); // true
System.out.println(map.get("key1")); // null
System.out.println(map.containsKey("key2")); // false
System.out.println(map.get("key2")); // null
// 使用 containsKey 区分"值为null"和"不存在"
if (map.containsKey("key1")) {
System.out.println("key1 存在,值为 null");
}
if (!map.containsKey("key2")) {
System.out.println("key2 不存在");
}
}
}
遍历包含 null key 的 Map
public class IterateNullKeyMap {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put(null, "nullValue");
map.put("key1", "value1");
map.put("key2", null);
// 方式1:entrySet 遍历
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() +
", Value: " + entry.getValue());
}
// 输出:
// Key: null, Value: nullValue
// Key: key1, Value: value1
// Key: key2, Value: null
// 方式2:keySet 遍历
for (String key : map.keySet()) {
System.out.println("Key: " + key +
", Value: " + map.get(key));
}
}
}
与其他 Map 实现的对比
1. HashMap vs Hashtable
public class HashMapVsHashtable {
public static void main(String[] args) {
// HashMap:允许 null key 和 null value
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "value1"); // ✓ 正常
hashMap.put("key1", null); // ✓ 正常
// Hashtable:不允许 null key 和 null value
Map<String, String> hashtable = new Hashtable<>();
try {
hashtable.put(null, "value1"); // ✗ NullPointerException
} catch (NullPointerException e) {
System.out.println("Hashtable 不支持 null key");
}
try {
hashtable.put("key1", null); // ✗ NullPointerException
} catch (NullPointerException e) {
System.out.println("Hashtable 不支持 null value");
}
}
}
Hashtable 源码:
public synchronized V put(K key, V value) {
// 不允许 null value
if (value == null) {
throw new NullPointerException();
}
// ...
int hash = key.hashCode(); // key 为 null 时抛出 NullPointerException
// ...
}
2. HashMap vs ConcurrentHashMap
public class HashMapVsConcurrentHashMap {
public static void main(String[] args) {
// HashMap:允许 null key
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "value1"); // ✓ 正常
// ConcurrentHashMap:不允许 null key 和 null value
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
try {
concurrentMap.put(null, "value1"); // ✗ NullPointerException
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap 不支持 null key");
}
try {
concurrentMap.put("key1", null); // ✗ NullPointerException
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap 不支持 null value");
}
}
}
ConcurrentHashMap 不支持 null 的原因:
// 在并发环境下,无法区分"值为null"和"key不存在"
V value = map.get(key);
if (value == null) {
// 问题:是 key 不存在,还是 value 本身就是 null?
// 需要再次调用 containsKey(key) 判断,但可能产生竞态条件
}
3. HashMap vs TreeMap
public class HashMapVsTreeMap {
public static void main(String[] args) {
// HashMap:允许 null key
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "value1"); // ✓ 正常
// TreeMap:不允许 null key(需要比较)
Map<String, String> treeMap = new TreeMap<>();
try {
treeMap.put(null, "value1"); // ✗ NullPointerException
} catch (NullPointerException e) {
System.out.println("TreeMap 不支持 null key(无法比较)");
}
// 但允许 null value
treeMap.put("key1", null); // ✓ 正常
}
}
TreeMap 不支持 null key 的原因:
// TreeMap 需要对 key 进行比较排序
public V put(K key, V value) {
// ...
int cmp = key.compareTo(parentKey); // null.compareTo() 会抛出 NPE
// ...
}
4. 对比总结
| Map 实现 | null key | null value | 说明 |
|---|---|---|---|
| HashMap | ✓ 支持(1个) | ✓ 支持(多个) | 非线程安全 |
| Hashtable | ✗ 不支持 | ✗ 不支持 | 线程安全(已过时) |
| ConcurrentHashMap | ✗ 不支持 | ✗ 不支持 | 线程安全,并发性能高 |
| TreeMap | ✗ 不支持 | ✓ 支持 | 有序,基于红黑树 |
| LinkedHashMap | ✓ 支持(1个) | ✓ 支持(多个) | 保持插入顺序 |
存储位置详解
null key 的存储机制
public class NullKeyStorageTest {
public static void main(String[] args) throws Exception {
Map<String, String> map = new HashMap<>(16);
// 插入 null key
map.put(null, "nullValue");
// 插入其他 key
map.put("key1", "value1");
map.put("key2", "value2");
// 通过反射查看存储位置
java.lang.reflect.Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
System.out.println("数组容量: " + table.length);
for (int i = 0; i < table.length; i++) {
if (table[i] != null) {
System.out.println("Index " + i + " 有元素");
// null key 总是在 index = 0
}
}
}
}
输出示例:
数组容量: 16
Index 0 有元素 ← null key 在这里
Index 5 有元素
Index 12 有元素
null key 与哈希冲突
public class NullKeyCollisionTest {
public static void main(String[] args) {
Map<SpecialKey, String> map = new HashMap<>(16);
// 插入 null key
map.put(null, "nullValue");
// 插入一个 hash 值为 0 的 key(会与 null key 冲突)
map.put(new SpecialKey(), "specialValue");
// 两者都在 table[0],形成链表或树
System.out.println(map.get(null)); // nullValue
System.out.println(map.get(new SpecialKey())); // specialValue
}
static class SpecialKey {
@Override
public int hashCode() {
return 0; // hash 为 0,与 null key 冲突
}
@Override
public boolean equals(Object obj) {
return obj instanceof SpecialKey;
}
}
}
存储结构:
table[0] → null key (nullValue) → SpecialKey (specialValue)
↑ 链表头
实战建议
1. 是否应该使用 null key?
不推荐使用 null key:
// 不推荐:语义不清晰
Map<String, User> userMap = new HashMap<>();
userMap.put(null, defaultUser); // null 代表什么?
// 推荐:使用明确的 key
Map<String, User> userMap = new HashMap<>();
userMap.put("default", defaultUser); // 语义清晰
userMap.put("anonymous", anonymousUser);
2. 处理可能为 null 的 key
public class SafeMapOperations {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// 方式1:使用 Optional
String key = null;
map.put(Optional.ofNullable(key).orElse("default"), "value1");
// 方式2:使用默认值
String safeKey = (key != null) ? key : "default";
map.put(safeKey, "value1");
// 方式3:使用工具类
map.put(Objects.toString(key, "default"), "value1");
}
}
3. 迁移到不支持 null key 的 Map
public class MigrateToNonNullKeyMap {
public static void main(String[] args) {
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "value1");
hashMap.put("key1", "value2");
// 迁移到 ConcurrentHashMap
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
for (Map.Entry<String, String> entry : hashMap.entrySet()) {
String key = entry.getKey();
// 将 null key 替换为特殊值
concurrentMap.put(
key != null ? key : "NULL_KEY_PLACEHOLDER",
entry.getValue()
);
}
}
}
面试答题总结
答题要点:
- 支持 null key:HashMap 允许 1 个 null key
- 存储位置:null key 的 hash 固定为 0,存储在 table[0]
- 比较方式:通过
==判断 null key,而非equals() - 其他 Map:Hashtable、ConcurrentHashMap、TreeMap 不支持 null key
加分项:
- 解释为什么 null key 的 hash 是 0
- 说明 null key 可能与其他 hash 为 0 的 key 冲突
- 对比不同 Map 实现对 null 的支持
- 提到 ConcurrentHashMap 不支持 null 是为了避免并发歧义
一句话总结:HashMap 允许一个 null key,hash 值固定为 0,存储在数组的第 0 个位置,这是与 Hashtable、ConcurrentHashMap 的重要区别。