前端视角 Java Web 入门手册 2.4.2:集合框架——Map

147 阅读5分钟

Map 是键值对存储类型的接口,键值唯一。HashMap、LinkedHashMap、TreeMap 实现类的主要区别是

  • HashMap 内部用哈希表实现,无法保证元素顺序
  • LinkedHashMap 继承自 HashMap,内部通过双链表维护元素插入顺序,做到了有序
  • TreeMap 使用红黑树,key 默认按照自然顺序排序,适用于需要根据键进行排序的场景

HashMap

HashMap 内部实现是一个数组+链表\红黑树的结合体

向 HashMap 插入一个元素时有几个步骤

  1. 根据 key 的哈希值,通过哈希函数计算出在数组中的索引位置
  2. 如果该索引位置还没有元素,则直接将该元素插入该位置
  3. 如果该索引位置已有元素,则遍历该索引位置上的链表,找到 key 值相同的节点,将其 value 值更新为新的值(key 相同,hash 值一定相同,新的值会替换旧的值)
  4. 如果该索引位置上的链表中没有 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());
}

如何选择

特性HashMapLinkedHashMapTreeMap
排序不支持不支持支持
插入顺序不保证保证不保证
查找效率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