目录
1. Map接口简介
在Java集合框架中,Map接口占据着重要的位置。Map用于存储键值对(key-value pairs),其中每个键都是唯一的。Map接口定义了一系列操作这些键值对的方法,如添加、删除、获取等。
主要的Map方法包括:
-
put(K key, V value)
: 添加键值对 -
get(Object key)
: 根据键获取值 -
remove(Object key)
: 移除键值对 -
containsKey(Object key)
: 检查是否包含某个键 -
containsValue(Object value)
: 检查是否包含某个值 -
size()
: 返回Map的大小 -
isEmpty()
: 检查Map是否为空
2. HashMap
HashMap是Map接口最常用的实现之一,它提供了高效的插入和查找操作。
2.1 基本特性
-
允许null键和null值
-
为什么ConcurrentHashMap不允许:他是专门为并发场景设计的Map实现,当我们get(key)操作返回null时,存在歧义:是key压根就不存在,还是该key存在,只是它的值为null。在单线程环境下,可以通过containsKey进行进一步判断,而在并发场景下,调用containsKey和get操作之间可能有其他线程对该key进行了修改。
-
-
不保证顺序
-
非同步(线程不安全)
-
初始容量和负载因子影响其性能
2.2 内部实现
HashMap在Java 8之前使用数组+链表实现,Java 8及以后使用数组+链表+红黑树实现。
-
哈希函数: HashMap使用键的hashCode()方法计算哈希值,然后通过哈希函数将其映射到数组索引。
-
解决冲突: 当多个键映射到同一个数组索引时,HashMap使用链表(或红黑树)来存储这些元素。
-
树化: 在Java 8中,当链表长度超过阈值(链表节点数为8,且哈希数组长度大于64)时,链表会转换为红黑树,以提高查找效率。
-
链表节点数为8,如果哈希数组长度小于64则不会进行树化而是进行扩容操作,主要是为了避免频繁树化和减少内存占用
-
当链表节点数小于6时,红黑树又会退化为链表,保持简单高效。设置为6而不是8时为了留个缓存空间,避免反复横跳
-
2.3 性能考虑
-
初始容量和负载因子(0.75)是影响HashMap性能的两个重要参数。
-
合理设置这两个参数可以减少rehash操作,提高性能。
2.4 代码示例
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
System.out.println(map.get("apple")); // 输出: 1
2.5 HashMap的扩容机制
HashMap的扩容是一个重要的操作,它直接影响HashMap的性能。当HashMap中的元素数量超过负载因子与当前容量的乘积时,就会触发扩容操作。
扩容过程
-
创建新数组: 创建一个新的数组,其容量是原数组的两倍。
-
重新哈希: 将原数组中的所有元素重新计算哈希值,并放入新数组中。
-
链表重排: 对于链表结构,需要重新排列链表中的元素(重新计算元素在哈希数组中的位置)。
-
树化或反树化: 在Java 8及以后版本中,还需要考虑红黑树的处理。
扩容时机
扩容发生在添加新元素后,如果满足以下条件:
if (++size > threshold)
resize();
其中,threshold
是触发扩容的阈值,通常等于容量 * 负载因子
。
2.6 HashMap的put操作流程
put操作是HashMap中最常用的操作之一,其流程如下:
-
计算哈希值: 对key调用hashCode()方法,并进行处理以减少碰撞;
-
确定桶位置: 根据哈希值确定在哈希数组中的位置 (哈希值与数组长度-1进行&运算);
-
初始化:如果数组是空的,则调用 resize 进行初始化;
-
查找或插入:
-
如果桶为空,直接插入新节点。
-
如果桶不为空,遍历链表或红黑树:
-
如果找到相同的key,更新value。
-
如果没找到,在链表末尾添加新节点或将新节点插入红黑树。
-
-
- 树化检查: 判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;
- 扩容检查: 插入新节点后,检查是否需要扩容。
另外要注意:hashCode方法和equals方法应同时进行重写,如果只重写了hashCode,那么在插入操作时,相同对象能够得到一样的哈希数组的位置,但是在进行对象比较时,相同对象会被认为是不同对象,从而导致hashMap中存储了两个一摸一样的对象。equals默认是比较对象的引用是否相等,即==运算比较。
2.7 为什么选择0.75作为默认负载因子
首先看看注释的解释:
大概的意思是:作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。
再详细地解释:HashMap的默认负载因子是0.75,这个值是经过权衡得出的最佳值,原因如下:
-
空间和时间的平衡:
-
较高的负载因子会减少空间开销,但会增加查找成本。
-
较低的负载因子会增加空间开销,但会减少查找成本。
-
0.75提供了一个很好的平衡点。
-
- 避免碰撞:
-
根据统计学原理,当负载因子为0.75时,哈希冲突的概率较低。
-
这意味着大多数桶中的元素数量会比较少,从而提高访问效率。
-
- 减少扩容频率:
-
0.75允许HashMap在扩容前存储更多元素,减少了扩容操作的频率。
-
这有助于提高整体性能,因为扩容是一个耗时的操作。
-
- 泊松分布:
-
在负载因子为0.75时,根据泊松分布,桶中元素个数为8的概率很小。
-
这就是为什么选择8作为将链表转换为红黑树的阈值。
-
3. Hashtable
Hashtable是Java早期提供的一个同步的Map实现。
3.1 特性
-
同步(线程安全)
-
不允许null键或null值(原因参考前面提到过的)
-
性能较HashMap差(因为同步)
3.2 与HashMap的比较
-
同步性: Hashtable是同步的,HashMap不是。
-
Null值: Hashtable不允许null键和值,HashMap允许。
-
性能: 在单线程环境下,HashMap通常优于Hashtable。
-
迭代器: HashMap的迭代器是fail-fast的,而Hashtable的枚举器不是。
3.3 使用场景
虽然Hashtable是线程安全的,但在现代Java应用中,通常推荐使用ConcurrentHashMap来代替Hashtable,因为ConcurrentHashMap提供了更好的并发性能(ConcurrentHashMap锁的粒度更小)。
4. ConcurrentHashMap
ConcurrentHashMap是专门为并发场景设计的Map实现。
4.1 特性
-
线程安全
-
高并发性能
-
不允许null键也不允许null值
-
分段锁设计(Java 7及之前)或CAS+Synchronized(Java 8及以后)
4.2 实现原理
-
Java 7: 使用分段锁(Segment)实现,将Map分为多个部分,每个部分独立加锁。
-
Java 8+: 使用CAS和synchronized来保证并发安全,synchronized只锁定当前链表或红黑树的首节点。
4.3 性能优势
-
读操作完全并发(volatile关键字)
-
写操作局部锁定
-
迭代不需要加锁
4.4 代码示例
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("one", 1);
concurrentMap.put("two", 2);
System.out.println(concurrentMap.get("one")); // 输出: 1
5. LinkedHashMap
LinkedHashMap是HashMap的一个子类,它在HashMap的基础上维护了一个双向链表,用于保持插入顺序或访问顺序。
5.1 特性
-
维护插入顺序或访问顺序(accessOrder,默认是false按照插入顺序,true按照访问顺序排序,即LRU)
-
允许null键和null值
-
性能略低于HashMap(因为要维护链表)
5.2 实现原理
LinkedHashMap在HashMap的基础上,为每个条目(Entry)增加了before和after指针,这些指针将所有条目连接成一个双向链表。
5.3 用途
-
需要按插入顺序遍历Map时
-
实现LRU(Least Recently Used)缓存
5.4 代码示例
Map<String, Integer> linkedMap = new LinkedHashMap<>();
linkedMap.put("one", 1);
linkedMap.put("two", 2);
linkedMap.put("three", 3);
for (Map.Entry<String, Integer> entry : linkedMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出:
// one: 1
// two: 2
// three: 3
6. 其他Map实现
6.1 TreeMap
-
基于红黑树实现
-
保持键的自然顺序或指定的比较器顺序
-
适用于需要按键排序的场景
6.2 WeakHashMap
-
键是弱引用
-
当键不再被强引用时,相应的条目会被自动移除
-
常用于缓存场景
6.3 IdentityHashMap
-
使用引用相等(==)而不是equals()来比较键
-
主要用于特殊场景,如序列化或深度复制
7. 使用场景比较
-
HashMap: 适用于大多数单线程场景,需要高效的插入和查找操作。
-
ConcurrentHashMap: 适用于高并发场景,需要线程安全的Map实现。
-
LinkedHashMap: 需要保持插入顺序或实现LRU缓存时使用。
-
TreeMap: 需要按键排序的场景。
-
Hashtable: 已被ConcurrentHashMap取代,不推荐使用。
-
WeakHashMap: 实现缓存,其中键对象可能会被垃圾回收。
8. 总结
Java提供了丰富的Map实现,每种实现都有其特定的用途和性能特征。在选择使用哪种Map实现时,需要考虑以下因素:
-
是否需要线程安全
-
键值对的数量和访问模式
-
是否需要保持特定的顺序
-
内存使用和垃圾回收的考虑
另外: