一、核心接口关系
classDiagram
class Map {
<<interface>>
+put(K,V)
+get(K)
+keySet()
+values()
}
class Set {
<<interface>>
+add(E)
+remove(E)
}
class Collection {
<<interface>>
}
Map <|-- HashMap
Map <|-- Hashtable
Collection <|-- Set
Set <|-- HashSet
HashMap ..|> Cloneable
HashMap ..|> Serializable
Hashtable ..|> Cloneable
Hashtable ..|> Serializable
二、HashMap vs HashTable
1. 核心区别对比
特性 | HashMap | Hashtable |
---|---|---|
线程安全 | 非线程安全(需手动同步) | 线程安全(方法级synchronized) |
性能 | 更高(无同步开销) | 较低(同步开销) |
Null键/值 | 允许1个null键和多个null值 | 不允许null键或值 |
继承体系 | 继承AbstractMap | 继承Dictionary(已过时) |
初始容量 | 16 | 11 |
扩容机制 | 2n | 2n+1 |
迭代器 | Fail-Fast | Fail-Safe |
Java版本 | 1.2引入 | 1.0引入 |
2. 底层实现原理
HashMap的put过程:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 表为空则初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算桶位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 3. 处理哈希冲突
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) // 红黑树处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 链表处理
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); // 链表转红黑树
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 4. 存在key则更新value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
++modCount;
// 5. 超过阈值则扩容
if (++size > threshold)
resize();
return null;
}
三、HashSet实现原理
1. 核心特点
-
基于HashMap实现(使用PRESENT对象作为虚拟value)
private transient HashMap<E,Object> map; private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; }
-
不允许重复元素(依赖HashMap的key唯一性)
-
无序集合(不保证迭代顺序)
-
允许null元素(但只能有一个)
2. 与HashMap的关系
graph LR
A[HashSet] -->|内部使用| B[HashMap]
B -->|Key存储元素| C[HashSet元素]
B -->|Value统一为PRESENT| D[Object常量]
四、Map与Set接口对比
1. 核心区别
特性 | Map接口 | Set接口 |
---|---|---|
存储结构 | 键值对(Key-Value) | 单元素 |
重复性 | Key唯一,Value可重复 | 元素唯一 |
实现类 | HashMap, TreeMap, LinkedHashMap | HashSet, TreeSet, LinkedHashSet |
查询方式 | 通过Key获取Value | 直接包含判断 |
典型用途 | 字典、缓存结构 | 去重、集合运算 |
2. 实现类性能对比
实现类 | 时间复杂度(平均) | 有序性 | 线程安全 |
---|---|---|---|
HashMap | O(1) | 无 | 不安全 |
LinkedHashMap | O(1) | 插入序 | 不安全 |
TreeMap | O(log n) | 排序序 | 不安全 |
HashSet | O(1) | 无 | 不安全 |
LinkedHashSet | O(1) | 插入序 | 不安全 |
TreeSet | O(log n) | 排序序 | 不安全 |
Hashtable | O(1) | 无 | 安全 |
五、高级特性与面试要点
1. HashMap扩容机制
-
默认负载因子:0.75(空间与时间的权衡)
-
扩容过程:
final Node<K,V>[] resize() { // 1. 计算新容量(2倍) // 2. 创建新数组 // 3. 重新哈希所有元素(Java8优化:高位低位拆分) }
-
树化阈值:链表长度≥8且桶数量≥64时转为红黑树
2. 线程安全替代方案
// 1. Collections工具类
Map<String,String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 2. ConcurrentHashMap(推荐)
ConcurrentHashMap<String,String> concurrentMap = new ConcurrentHashMap<>();
// 3. CopyOnWriteArraySet(适合读多写少场景)
Set<String> safeSet = new CopyOnWriteArraySet<>();
3. 常见面试问题解析
Q1:HashMap为什么线程不安全?
-
并发修改问题:多线程put可能导致数据丢失
-
resize死循环(JDK1.7之前):链表rehash时形成环形链
-
解决方案:使用ConcurrentHashMap或Collections.synchronizedMap
Q2:HashSet如何保证元素唯一性?
-
依赖HashMap的key唯一性机制
-
实际比较流程:
// 1. 先比较hashCode() // 2. 如果hashCode相同,再调用equals()
Q3:LinkedHashMap如何保持顺序?
// 内部维护双向链表
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 添加前驱后继指针
}
Q4:TreeMap的排序实现原理?
-
基于红黑树(自平衡二叉查找树)
-
排序方式:
// 1. 自然排序(实现Comparable) // 2. 定制排序(传入Comparator)
六、最佳实践建议
-
初始化容量优化:
// 预估元素数量/0.75 + 1 new HashMap<>(expectedSize * 4 / 3 + 1);
-
对象作为Key的要求:
-
重写
hashCode()
和equals()
方法 -
保证不可变性(否则hash值变化导致找不到元素)
-
-
遍历优化:
// 推荐方式(entrySet遍历) for (Map.Entry<K,V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue(); }
-
Java8+新特性:
// 合并操作 map.merge(key, value, (oldVal, newVal) -> oldVal + newVal); // 统计单词频率 Map<String, Integer> freq = words.stream() .collect(Collectors.toMap(w -> w, w -> 1, Integer::sum));