LinkedHashMap 学习总结

LinkedHashMap简介

我们知道 HashMap 是 数组+链表/红黑树 的存储结构,它不能保证遍历顺序和插入顺序的一致。

LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构 数组+链表/红黑树,不同的是: LinkedHashMap内部维护了一个双向链表 LinkedHashMapEntry<K,V> ,用来保证添加顺序与插入顺序得到一致性。同时,LinkedHashMap 使用成员变量 accessOrder 来控制是否使用访问顺序。

LinkedHashMap可以用来实现LRU算法,LinkedHashMap同样是非线程安全的。

LinkedHashMap源码分析总结

双向链表 LinkedHashMapEntry

内部维护一个双向链表,用来记录节点的添加顺序。

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after; // 双向链表的前后指针
    public LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

put(K k, V v) 添加数据并构建双向链表

LinkedHashMap 没有重写 put 方法,因此我们还是去看它的基类 HashMap 的put

HashMap.java
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    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 ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果对应槽位上没有Node,则新建一个 Node 并覆盖该槽位
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else { // 如果发生 hash 碰撞
            Node<K,V> e; K k;
            
            // 如果新添加的节点是单链表的 head 节点,则不去遍历
            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) // 如果单链表的长度大于等于7,则改用红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 遍历过程中,找到对应的Node,需要替换该节点的value, 结束循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 替换所指节点 e 的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 如果散列表中不存在与该 key 匹配的节点,则:操作数增加,如果size超出阙值,需要进行扩容
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
LinkedHashMap.java

    transient LinkedHashMapEntry<K,V> head; // 双链表的头节点
    transient LinkedHashMapEntry<K,V> tail; // 双链表的尾结点

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMapEntry<K,V> p =
            new LinkedHashMapEntry<K,V>(hash, key, value, e);
        // 将 LinkedHashMapEntry 插入双链表的尾部
        linkNodeLast(p);
        return p;
    }

总结

当我们通过 put(k,v) 向该数据结构添加键值对时,通过对 key 进行hash计算得到对应散列表的槽位(数组的索引), 根据索引在数组中以 O(1) 的时间复杂度查找元素 Node<K,V>, 查找规则:如果对应索引上没有元素,则新建Node并覆盖在该槽位上;否则,说明出现了 hash 碰撞,此时需要遍历单链表/红黑树, 如果存在 **(k=p.key)==key||(key!=null&&key.equals(k)))**的节点,则更新该节点的 value, 否则新建节点插入单链表尾部。最后,如果 size>throld 则进行扩容。

LinkedHashMap 在以上的基础上,重写了 newNode(),将新添加的节点插入了双链表的尾部,并移动 tail 指针到新添加节点。

get(K key) 访问数据

HashMap.java
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    void afterNodeAccess(Node<K,V> node);

LinkedHashMap.java
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 如果需要维护访问顺序,则把tail 节点 移动到该节点 e
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        // 如果需要维护访问顺序且访问的节点 e 不是双向链表的尾结点
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            // 更新双向链表,且将访问节点置为尾结点
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            // 操作数加一
            ++modCount;
        }
    }

从上面的代码可以看出:HashMap 与 LinkedHashMap 在获取对应 key 的 value 时,只有在 accessOrder == true ,即维护访问顺序的时候有区别,其他地方完全一样。

通过 afterNodeAccess() 操作,我们将访问的节点从原来位置处移除并添加到了双向链表的尾部,如此一来,双向链表就维护了散列表中元素的访问顺序。即:我们最近访问到的数据在迭代访问时,总是最后一个出现。因此,我们很容易实现一个 LRUCache(最近最少使用被淘汰算法)。

class LRUDemo {
    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(3);
        
        lruCache.put("1",1);
        lruCache.put("2",2);
        lruCache.put("3",3);
        
        print(lruCache);
        
        System.out.println(String.format("key 1 value %s", map.get("1")));
        map.put("4", 4);
        
        print(lruCache);
    }
    private static void print(Map<String,Integer> map) {
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();
        // 防止出现多线程并发
        synchronized (map){
            while (iterator.hasNext()) {
                Map.Entry<String, Integer> next = iterator.next();
                System.out.println(String.format("key = %s ; value = %s ",next.getKey(), next.getValue()));
            }
        }
        System.out.println("------");
    }
    private static class LRUCache<K,V> extends LinkedHashMap<K,V> {
        private static final int DEFAULT_MAX_SIZE = 100;
        private int maxSize;
        public LRUCache() {
            this(DEFAULT_MAX_SIZE);
        }
        public LRUCache(int maxSize) {
            super(maxSize, 0.75, true);
            this.maxSize = maxSize;
        }
        @override protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return size() > maxSize;
        }
    }
}

问题

  1. HashMap 与 LinkedHashMap 的迭代器,谁的访问效率更高?

其实,从两者不同点之一:HashMap 没有记录添加顺序,LHM 通过双向链表记录了添加顺序。不难看出,LinkedHashMap 的迭代访问效率要比HashMap要高。因为,HashMap需要先遍历数组tab(O(n)),然后再遍历单链表(O(l))/红黑树(O(log l)), 总的时间复杂度为 O(n * l) 。l为单链表的长度,n 为table 的长度; 而LinkedHashMap 只需要 O(size), size < n。有兴趣的可以看迭代器的 nextNode() 实现。

  1. LinkedHashMap 比 HashMap 的 containsValue() 效率更高。 因为 HashMap需要先遍历 table 再遍历 单链表;而LinkedHashMap 只要遍历一遍 双向链表。因此两者的时间复杂度分别为O(n * l) 和 O(size)

remove(key) 删除指定key 的节点

和添加节点操作一样,LHP没有重写 remove(),使用的依然是 HashMap 的remove()。

HashMap.java
    public V remove(Object key) {
        Node<K,V> e;
        // 通过hash函数计算得出key在数组中对应的槽位,然后执行 removeNode并返回 value
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            // 如果单链表的 head 就是我们要移除的节点,则不进行遍历
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                // 遍历单链表/红黑树,找到对应的节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 如果找到与key对应的节点,则进行移除
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p) // 如果是单链表的 head, 则将 head.next 赋值到数组对应 index 上
                    tab[index] = node.next;
                else // 如果是中间节点,则将前一节点 p 的下一节点指向对应节点的下一节点
                    p.next = node.next;
                // 操作数加一, size 减一
                ++modCount;
                --size;
                // 更新双向链表
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
LinkedHashMap.java

    void afterNodeRemoval(Node<K,V> e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        // 如果删除的节点是双向链表的头结点,则将头结点的下一节点更新为头结点,否则将前一节点的后节点指向当前节点的后一个节点
        if (b == null)
            head = a;
        else
            b.after = a;
        // 如果删除的节点是双向链表的尾结点,则将当前结点的前一节点更新为尾结点,否则将后一节点的前节点指向当前节点的前一个节点
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

总结:

删除指定的key 的节点:1. 通过 hash 函数计算key在数组中的索引;2. 遍历该索引在数组中的单链表/红黑树,删除指定的节点,并返回节点值。

LinkedHashMap 在执行删除操作后,需要更新双向链表,保证数据的顺序性。

总结

  1. LinkedHashMap 继承至 HashMap,两者使用相同的存储结构 数组 + 单链表/红黑树,也使用相同的扩容机制,只是 LinkedHashMap 在内部使用双向链表维护了添加顺序,并使用 accessOrder 属性控制了访问顺序。所以,LinkedHashMap 比 HashMap 消耗更多的内存,而在访问效率上,LinkedHashMap比HashMap要高,因此,这是一个牺牲空间换取时间的策略。
  2. LinkedHashMap 的访问顺序默认情况下与添加顺序一致,而 HashMap 的访问顺序不一定与添加顺序一致。
  3. LinkedHashMap 与 HashMap 都是非线程安全的,因此在使用迭代器遍历数据的时候,必须使用锁机制来避免并发问题。

问题

  1. LinkedHashMap 使用 双向链表 维护节点的添加顺序,那么在 put 时,需要扩容的话,是不是更容易产生环形链表导致CPU暴增?Java 是如何解决这个问题的?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值