Map 是键值对存储类型的接口,键值唯一。HashMap、LinkedHashMap、TreeMap 实现类的主要区别是
- HashMap 内部用哈希表实现,无法保证元素顺序
- LinkedHashMap 继承自 HashMap,内部通过双链表维护元素插入顺序,做到了有序
- TreeMap 使用红黑树,key 默认按照自然顺序排序,适用于需要根据键进行排序的场景
HashMap
HashMap 内部实现是一个数组+链表\红黑树
的结合体
向 HashMap 插入一个元素时有几个步骤
- 根据 key 的哈希值,通过哈希函数计算出在数组中的索引位置
- 如果该索引位置还没有元素,则直接将该元素插入该位置
- 如果该索引位置已有元素,则遍历该索引位置上的链表,找到 key 值相同的节点,将其 value 值更新为新的值(key 相同,hash 值一定相同,新的值会替换旧的值)
- 如果该索引位置上的链表中没有 key 值相同的节点,则创建新的节点,如果链表长度大于 8 则转换为红黑树,然后将其插入到链表或红黑树
Map<Integer, String> hm = new HashMap<Integer, String>();
hm.put(1, "John");
hm.put(2, "Marry");
hm.put(3, "Bob");
hm.put(3, "Lisa"); // 替换
hm.put(4, "Byron");
String name = hm.get(3);
System.out.println("Value at key 3 is: " + name);
hm.remove(2);
for (Integer key : hm.keySet()) {
System.out.println("key: " + key + ", Value: " + hm.get(key));
}
当 HashMap 中存储的元素数量超过了容量 * 负载因子(默认 0.75)
时 HashMap 会自动扩容,以保证哈希表的负载因子不会过大,从而维护较好的性能
当 HashMap 要进行扩容时,会新建一个容量为原来的两倍的数组,然后将原来数组中的元素重新计算哈希值,放入新数组中的相应位置。这个过程需要遍历原来数组中的所有元素,HashMap 会将原来的键值对数组复制到新数组中
如果存储在 HashMap 中的对象比较大,扩容会造成较大的开销,因此在创建 HashMap 时为避免频繁扩容应该预估存储对象的数量,并合理地设置初始容量 public HashMap(int initialCapacity, float loadFactor)
LinkedHashMap
LinkedHashMap 是一个基于哈希表和双向链表实现的 Map,它继承自 HashMap,具有 HashMap 的所有特性。与 HashMap 不同的是 LinkedHashMap 内部追加了一个双链表来维护元素的插入顺序
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
在 LinkedHashMap 中当使用 put() 方法向 LinkedHashMap 中添加键值对时,会将新节点插入到链表的尾部,并更新 before 和 after 属性;同时维护 head、tail 两个指针,head 表示最早插入或访问的元素,tail 表示最晚插入或访问的元素,这样就可以保证链表的顺序关系
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
LinkedHashMap 不仅能够维持插入的顺序,还能够维持访问顺序,元素被访问(get、remove、put)后顺序移动到最后。要维护访问顺序需要声明 LinkedHashMap 的时候指定第三个参数为 true
LinkedHashMap<String, String> map = new LinkedHashMap<>(16, .75f, true);
这个特性非常适合实现 LRU(Least Rencently Used) 缓存,即最近最少使用,当缓存池满的时候删除掉最近没有使用过的元素
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int cacheSize;
public LRUCache(int cacheSize) {
// 调用父类的构造函数,设置accessOrder为true,即按照访问顺序存储
super(16, 0.75f, true);
this.cacheSize = cacheSize;
}
// 重写父类的removeEldestEntry方法,当缓存元素个数大于等于cacheSize时,删除最早访问的元素
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() >= cacheSize;
}
}
TreeMap
TreeMap 基于红黑树实现,键按照自然顺序或者指定的比较器进行排序,同时 TreeMap 提供了几个有用的方法可以方便获取最大、最小及一定范围内的 key,特别适用于需要对 key 排序的场景
TreeMap 最大的特点之一就是能够自动对键进行排序,当向 TreeMap 中插入元素时,它会根据键的排序规则自动将元素放置在合适的位置。
- firstKey():获取第一个 key
- lastKey():获取最后一个 key
- headMap():获取指定 key 之前的 treeMap
- tailMap():获取指定 key 之后的 treeMap
// 创建一个 TreeMap 对象
Map<Integer, String> treeMap = new TreeMap<>();
// 添加映射关系
treeMap.put(3, "orange");
treeMap.put(1, "apple");
treeMap.put(4, "grape");
treeMap.put(2, "banana");
// 输出所有映射关系
for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + " => " + entry.getValue());
}
// 返回键值大于等于 2 的所有映射关系
SortedMap<Integer, String> tailMap = treeMap.tailMap(2);
System.out.println("键值大于等于 2 的所有映射关系:");
for (Map.Entry<Integer, String> entry : tailMap.entrySet()) {
System.out.println(entry.getKey() + " => " + entry.getValue());
}
如何选择
特性 | HashMap | LinkedHashMap | TreeMap |
---|---|---|---|
排序 | 不支持 | 不支持 | 支持 |
插入顺序 | 不保证 | 保证 | 不保证 |
查找效率 | O(1) | O(1) | O(log n) |
空间占用 | 小 | 大 | 大 |
适用场景 | 无特定需求就选它 | 需要保持插入顺序 | 需要按 key 排序的场景 |
ConcurrentHashMap
ConcurrentHashMap 是 Map 接口的高性能并发实现,适用于多线程环境。它通过分段锁机制,允许多个线程并发访问不同段,提高了性能。
在多线程环境中,多个线程可能会同时访问和修改共享资源。由于线程的执行顺序是不确定的,可能会出现一个线程在修改共享资源的过程中,另一个线程也来访问或修改该资源,从而导致数据的不一致性或程序逻辑的错误。线程安全是在多线程编程领域中的一个重要概念,它描述了一个对象、方法或系统在多线程环境下能够正确、稳定地工作,不会因为多个线程的并发访问而产生数据不一致、逻辑错误等问题
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建 ConcurrentHashMap 对象
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 插入键值对
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
// 获取值
Integer value = map.get("banana");
System.out.println("Value of banana: " + value);
// 遍历 ConcurrentHashMap
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 删除键值对
map.remove("cherry");
System.out.println("After removing cherry: " + map);
}
}
Hashtable 是一个线程安全的哈希表实现,但由于其同步机制带来的性能开销,在现代 Java 开发中,如果不需要线程安全,通常推荐使用 HashMap;如果需要在多线程环境下使用哈希表,建议使用 ConcurrentHashMap