LinkedHashMap源码解析

LinkedHashMap 直接继承自HashMap

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

而LinkedHashMap比HashMap优于以下几点

  • LinkedHashMap 内部维护了一个双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题
  • LinkedHashMap 元素的访问顺序也提供了相关支持,也就是我们常说的 LRU(最近最少使用)原则。

LinkedHashMap有两个因子影响着其性能:初始容量和负载因子。它们的定义与HashMap完全相同。要注意,为初始容量选择非常高的值对此类的影响比对HashMap要小,因为此类的迭代时间不受容量的影响。

1、类成员
final boolean accessOrder;

如果没有特别指定排序模式,那么accessOrder = false,因此其默认将按照插入顺序来作为迭代顺序。如果设置为true,则使双向链表维护哈希表中元素的访问顺序

2、构造方法
/**
 * 根据指定的初始容量和负载因子,初始化一个空的按照插入顺序排序的 LinkedHashMap 的实例
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/**
 * 根据指定的容量和默认的负载因子(0.75),初始化一个空的按照插入顺序排序的 LinkedHashMap 的实例
 */
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

/**
 * 根据默认的容量(16)和负载因子(0.75),初始化一个空的按照插入顺序排序的 LinkedHashMap 实例
 */
public LinkedHashMap() {
    super();
    accessOrder = false;
}

/**
 * 初始化一个根据传入的映射关系并且按照插入顺序排序的 LinkedHashMap 的实例
 * 这个 LinkedHashMap 实例的负载因子为0.75,容量不小于指定的映射关系的数量的最小2次幂
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

/**
 * 根据指定的容量、负载因子、排序模式来初始化一个空的 LinkedHashMap 的实例
 * accessOrder 为 true 时按条目访问顺序作为迭代顺序,为 false 时按照插入顺序作为迭代顺序
 */
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
3、节点

LinkedHashMap 对于 HashMap.Node 节点进行了拓展:

    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在HashMap的基础上添加了 before 和 after 这两个指针变量。这 before 变量在每次添加元素的时候将会链接上一次添加的元素,而上一次添加的元素的 after 变量将指向该次添加的元素,来形成双向链接。值得注意的是 LinkedHashMap 并没有覆写任何关于 HashMap put 方法。所以调用 LinkedHashMap 的 put 方法实际上调用了父类 HashMap 的方法。

4、三个重要的回调函数

在HashMap源码中,预留了三个回调函数,来让LinkedHashMap进行后期操作:

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

在LinkedHashMap中,这三个函数实现如下:

//移除节点的时候会触发回调,将节点从双向链表中删除,在调用 removeNode 函数时候会执行
void afterNodeRemoval(Node<K, V> e) { // unlink
    LinkedHashMap.Entry<K, V> p =
        (LinkedHashMap.Entry<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;
}

//新节点插入时会触发回调,根据条件判断是否移除最老的条目,在调用 compute computeIfAbsent merge putVal 函数时候会实行
//实现 LruCache 的时候会用到这个函数
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K, V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

//将节点放置链表尾,在调用 putVal 函数时会执行,保证最近访问节点在链表尾部
void afterNodeAccess(Node<K, V> e) { // move node to last
    LinkedHashMap.Entry<K, V> last;
    //accessOrder为 true表示按照访问顺序排序,并且此时的键值对不在链表尾部
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K, V> p =
            (LinkedHashMap.Entry<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;
    }
}

从上面三个回调函数可以看出,其主要是在对条目进行操作的时候触发来维护双向链表。另外值得一提的是afterNodeInsertionremoveEldestEntry函数,在构建 LruCache 时将非常有用。对于removeEldestEntry,其默认返回false,因此默认情况下不会删除最旧的元素:

/**
 * @param    eldest 哈希表中最近插入的条目,或者如果迭代顺序是按照访问顺序排序,则是最近最少访问的条目。
 *                  如果这个方法返回 true,则这是将被删除的条目。如果在 put 或 putAll 调用之前哈希表为空时,触发此调用,
 *                  则这将是刚插入的条目;换句话说,如果哈希表包含单个条目,则最老的条目也是最新的。
 * @return   返回 true 表明将删除最老的条目
 */
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return false;
}

如果需要删除最旧条目,则返回true。在将新条目插入后,put和putAll将调用此方法。它为实现者提供了在每次添加新条目时删除最旧条目的机会。如果用来实现缓存,则此选项非常有用:它允许哈希表通过删除过时条目来减少内存消耗。

5、put插入

LinkedHashMap直接使用了HashMap的put函数,但重写了newNode、afterNodeAccess和afterNodeInsertion方法。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    //将节点放置链表尾部
    linkNodeLast(p);
    return p;
}

// 将新增节点放置链表尾部
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;
    }
}

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;
   if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
   else {// 发生 hash 碰撞了
       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){....}
       else {
          //hash 值计算出的数组索引相同,但 key 并不同的时候 循环整个单链表
           for (int binCount = 0; ; ++binCount) {
               if ((e = p.next) == null) {//遍历到尾部
                    // 创建新的节点,拼接到链表尾部
                   p.next = newNode(hash, key, value, null);
                   ....
                   break;
               }
               //如果遍历过程中找到链表中有个节点的 key 与 当前要插入元素的 key 相同,
               //此时 e 所指的节点为需要替换 Value 的节点,并结束循环
               if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   break;
               //移动指针    
               p = e;
           }
       }
       //如果循环完后 e!=null 代表需要替换e所指节点 Value
       if (e != null) {
           V oldValue = e.value//保存原来的 Value 作为返回值
           // onlyIfAbsent 一般为 false 所以替换原来的 Value
           if (!onlyIfAbsent || oldValue == null)
               e.value = value;
           afterNodeAccess(e);//该方法在 LinkedHashMap 中的实现稍后说明
           return oldValue;
       }
   }
   //操作数增加
   ++modCount;
   //如果 size 大于扩容阈值则表示需要扩容
   if (++size > threshold)
       resize();
   afterNodeInsertion(evict);
   return null;
}

看出每次添加新节点的时候实际上是调用 newNode 方法生成了一个新的节点,放到指定 hash 桶中,但是很明显,HashMap 中 newNode 方法无法完成上述所讲的双向链表节点的间的关系,所以 LinkedHashMap 复写了该方法。
我们创建一个新节点之后,通过linkNodeLast方法,将新的节点与之前双向链表的最后一个节点(tail)建立关系,在这部操作中我们仍不知道这个节点究竟储存在哈希表表的何处,但是无论他被放到什么地方,节点之间的关系都会加入双向链表。

6、删除

LinkedHashMap仍然直接使用了HashMap的remove函数,只是对afterNodeRemoval回调函数进行了重写

 public V remove(Object key) {
   Node<K,V> e;
   return (e = removeNode(hash(key), key, null, false, true)) == null ?
       null : e.value;
}

// HashMap 中实现
 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;
   //判断哈希表是否为空,长度是否大于0 对应的位置上是否有元素
   if ((tab = table) != null && (n = tab.length) > 0 &&
       (p = tab[index = (n - 1) & hash]) != null) {
       
       // node 用来存放要移除的节点, e 表示下个节点 k ,v 每个节点的键值
       Node<K,V> node = null, e; K k; V v;
       //如果第一个节点就是我们要找的直接赋值给 node
       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);
           }
       }
       // 如果找到了节点
       // !matchValue 是否不删除节点
       // (v = node.value) == value ||
                            (value != null && value.equals(v))) 节点值是否相同,
       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)
               tab[index] = node.next;
           else
               p.next = node.next;
           ++modCount;
           --size;
           afterNodeRemoval(node);// 注意这个方法 在 Hash表的删除操作完成调用该方法
           return node;
       }
   }
   return null;
}

//  从双向链表中删除对应的节点 e 为已经删除的节点
void afterNodeRemoval(Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 将 p 节点的前后指针引用置为 null 便于内存释放
    p.before = p.after = null;
    // p.before 为 null,表明 p 是头节点 
    if (b == null)
        head = a;
    else//否则将 p 的前驱节点连接到 p 的后驱节点
        b.after = a;
    // a 为 null,表明 p 是尾节点
    if (a == null)
        tail = b;
    else //否则将 a 的前驱节点连接到 b 
        a.before = b;
}


7、get查询
/**
 * 返回指定 key 所对应的 value 值,当不存在指定的 key 时,返回 null。
 *
 * 当返回 null 的时候并不表明哈希表中不存在这种关系的映射,有可能对于指定的 key,其对应的值就是 null。
 * 因此可以通过 containsKey 来区分这两种情况。
 */
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

与HashMap相比,其多了一步对 accessOrder 的判断来维护链表,当指定迭代顺序按照访问顺序排序时,get操作表明对指定的条目进行了一次访问,那么此条目应该移到链表尾部。对于afterNodeAccess在上面已经分析过了,值得注意的是,在调用afterNodeAccess时,会修改 modeCount,所以当你正在accessOrder = true的模式下迭代LinkedHashMap时,如果同时查询访问数据,会导致 fail-fast,因为迭代的顺序已经变了。

8、containsValue
//LinkedHashMap 中 containsValue 的实现
public boolean containsValue(Object value) {
    // 直接遍历双向链表去寻找对应的节点
   for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
       V v = e.value;
       if (v == value || (value != null && value.equals(v)))
           return true;
   }
   return false;
}
//HashMap 中 containsValue 的实现
public boolean containsValue(Object value) {
   Node<K,V>[] tab; V v;
   if ((tab = table) != null && size > 0) {
        //遍历 哈希桶索引
       for (int i = 0; i < tab.length; ++i) 
            //遍历哈希桶中链表或者红黑树
           for (Node<K,V> e = tab[i]; e != null; e = e.next) {
               if ((v = e.value) == value ||
                   (value != null && value.equals(v)))
                   return true;
           }
       }
   }
   return false;
}

于LinkedHashMap其与HashMap还有一些不同,由于LinkedHashMap维护一个双向链表,因此在判断哈希表中是否存储着某个键值对的时候,不需要在整个数组桶中查找,而只需要对链表遍历即可,这也是LinkedHashMap的其中一处优化。

9、实现 LruCache

在 LeetCode 有一道题——Lru Cache:设计和实现一个 LRU (最近最少使用) 缓存机制,那么就可以利用LinkedHashMap可选的迭代顺序——按访问顺序的模式来进行实现:

class LRUCache {
    private int capacity;
    private Map<Integer, Integer> cache;
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new java.util.LinkedHashMap<Integer, Integer> (capacity, 0.75f, true) {
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                return size() > capacity;
            }
        };
    }
    
    public int get(int key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        } else
            return -1;
    }
    
    public void put(int key, int value) {
        cache.put(key, value);
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

当然,如果觉得直接使用LinkedHashMap的方式太过取巧,我们仍可以借鉴LinkedHashMap的思想来进行实现——使用 HashMap 和 双向链表 的组合来实现:

class LRUCache {
    class Node{
        Integer key;        
        Integer value;
        Node prev;
        Node next;

        public Node(Integer key, Integer value){
            this.key = key;
            this.value = value;
        }
    }

    private Map<Integer, Node>map;
    Node head;
    Node tail;
    int size;

    public LRUCache(int capacity) {
        size = capacity;
        map = new HashMap<>(capacity);
        head = new Node(null, null);
        tail = new Node(null, null);

        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        Node node = map.get(key);
        if (null != node){
            map.remove(node.key);

            node.prev.next = node.next;
            node.next.prev = node.prev;

            appendTail(node);
            map.put(key, node);
        }

        int value = null == node ? -1 : node.value;
        return value;
    }
    
    public void put(int key, int value) {
        Node node = map.get(key);
        if (null != node){
            map.remove(node.key);

            node.prev.next = node.next;
            node.next.prev = node.prev;

            node.value = value;
        }else if (map.size() == size){
            Node tmp = head.next;
            map.remove(tmp.key);

            head.next = tmp.next;
            tmp.next.prev = head;

            tmp = null;
        }

        if (null == node)   node = new Node(key, value);
        appendTail(node);
        map.put(key, node);
    }

    public void appendTail(Node node){
        tail.prev.next = node;
        node.prev = tail.prev;
        node.next = tail;
        tail.prev = node;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

  1. LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
  2. LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外通过 Entry 维护了一个双向链表。
  3. HashMap 元素的遍历顺序不一定与元素的插入顺序相同,而 LinkedHashMap 则通过遍历双向链表来获取元素,所以遍历顺序在一定条件下等于插入顺序。
  4. LinkedHashMap 可以通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值