Java从入门到放弃(十五)集合框架之LinkedHashMap源码

LinkedHashMap是一个有序的HashMap,各个节点以双向链表的形式链接着,结构如图所示:LinkedHashMap结构图
LinkedHashMap的真实结构不会死这样子的,因为长度原因截图部分。节点里的数字标识实际上是没有的,是为了表示插入的顺序(如果是jdk1.7图会有不同,因为1.8之前hashmap是头插入,1.8之后是尾插入)。每一个节点就是一个内部类Entry对象,这个结构图和HashMap的结构图基本上是一样的,LindedHashMap和HashMap的区别是:LinkedHashMap还需要维护插入的顺序,节点之间的绿色的线是after指向,红色的线是before指向。

属性

   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);
        }
    }
    transient LinkedHashMap.Entry<K,V> head;   //链表头结点
    transient LinkedHashMap.Entry<K,V> tail;   //链表尾节点
    final boolean accessOrder;    //实现LRU

LinkedHashMap继承了HashMap,本质上来说,它还是一个HashMap,只是多维护了几个属性而已。主要通过Entry不同来实现。如上,Entry继承了HashMap.Entry,多了before和after属性,然后比起HashMap还多了head和tail两个属性,指向链表的头尾节点。

构造方法

    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);  //容量,负载因子
        accessOrder = false;
    }
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

这里构造方法都是调用了父类的构造方法,然后accessOrder默认是false,accessOrder设置为true时,链表会实现LRU,把最近访问的节点放到链表的末尾。对于HashMap的方法这里不再讲述,具体参考 Java从入门到放弃(十)集合框架之HashMap源码(1)

put方法

这里并没有重写put方法调用的还是HashMapd的put方法。如下:

      public V put(K key, V value) {  
           return putVal(hash(key), key, value, false, true);  
      }  
//hash就是key的hash值,根据这个hash得出数据存放再数组中的位置,onlyIfAbsent为true时,不存在key才会执行put操作。  
      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)   // 初始化是一个空的Node数组  
               n = (tab = resize()).length;   // 扩容方法,初始化扩容默认容量为16  
           if ((p = tab[i = (n - 1) & hash]) == null)  //如果数组索引处为null  
               tab[i] = newNode(hash, key, value, null);    //就把key-value构造一个Node数组插入数组中  
           else {  
               Node<K,V> e; K k;  
               if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))  //如果hash和key都相等  
                   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);   // 把新的Node节点插入链表的尾部(尾插法,1.8之前是头插入)  
                           if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   //如果链表长度大于等于8,转换为红黑树  
                               treeifyBin(tab, hash);  
                           break;  
                       }  
                       if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))  //已经插入完成,直接退出循环  
                           break;  
                       p = e;  
                   }  
               }  
               if (e != null) { // existing mapping for key,如果存在对应的key值,直接进行更新操作  
                   V oldValue = e.value;  
                   if (!onlyIfAbsent || oldValue == null)    // onlyIfAbsent为false,就更新  
                       e.value = value;  
                   afterNodeAccess(e);  
                   return oldValue;  //返回旧值  
               }  
           }  
           ++modCount;         //修改记录值  
           if (++size > threshold)   //size大于负载因子要求的容量值就进行扩容  
               resize();  
           afterNodeInsertion(evict);  
           return null;  
   }  

不同的地方在于LinkedHashMap重写了newNode方法

     Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<>(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)    //如果尾节点为null,说明集合是空的,把head也指向新的节点
            head = p;
        else {             //如果集合不为空
            p.before = last;    //把p的before指向之前的尾节点
            last.after = p;     //把之前的尾节点的after指向p
        }
    }

和LinkedList的操作是一样的,就是重新设置tail以及节点的before和after指向。具体可查看Java从入门到放弃(八)集合框架之LinkedList源码(1)
这里有两个方法afterNodeAccessafterNodeInsertion方法,在HashMap里面这是两个空的方法,LinkedHashMap里面的具体实现如下:

      //方法是把e对应的节点移到链表的末尾
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
             LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;    //e转换为Entry对象
            p.after = null;     //设置p的after为null,
            if (b == null)
                head = a;     //如果是p为 first节点,把p的after设为首节点  
            else
                b.after = a;          //把p的befor节点的after指向p的after指向的节点
            if (a != null)
                a.before = b;       //p不是尾节点,就把p的after节点的before指向p的before指向的节点
            else
                last = b;             //如果是尾节点,就把last指向p的before指向的节点
            if (last == null)
                head = p;         //如果last为null,即  集合为空,把head指向p
            else {
                p.before = last;         //p的before指向之前的tail节点
                last.after = p;            //之前的tail的after指向p节点
            }
            tail = p;
            ++modCount;
        }
    }

就是为了维护链表,把新插入的数据放在链表的尾部。
看一下afterNodeInsertion方法:

   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);
        }
    }
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

这里LinkedHashMap并没有实际操作,因为removeEldestEntry是一直返回false,我们可以自己继承重写这个方法,这样子可以把头节点删除,实现LRU,把集合内的数据维持在一个固定的数,经常作为缓存topN使用。就是把最少使用的数据删除。

remove方法

这里不贴remove的方法,就是HashMap的remove方法,可参考之前的文章,唯一的不同是afterNodeRemoval方法,在HashMap里是空方法,LinkeHashMap里重写了这个方法:

    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;   把p的before,after引用都置为null,便于GC
        if (b == null)
            head = a;   
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

其实这个方法和前面的一些逻辑是一样的,就是把删除节点的前节点和后节点之间的引用进行更改。把删除节点的引用都置为null,便于GC;

get方法

   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;
    }
    public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }

这两个方法唯一的区别是getOrDefault在找不到对应的key时,会返回第二个参数。这里还是调用了HashMap的get方法。只是在开启LRU的模式下会把对应的节点移到链表的末尾。

迭代遍历示例

        LinkedHashMap<String,Integer> linkedHashMap = new LinkedHashMap(8,0.75f,true);
        linkedHashMap.put("a",1);
        linkedHashMap.put("b",2);
        linkedHashMap.put("c",3);
        linkedHashMap.put("d",4);
        linkedHashMap.put("e",5);
        Set<String> str1 = linkedHashMap.keySet();
        System.out.println(str1);    //[a, b, c, d, e]

        linkedHashMap.get("b");
        linkedHashMap.get("c");
        Set<String> str2 = linkedHashMap.keySet();
        System.out.println(str2);    //[a, d, e, b, c]

        linkedHashMap.put("a",6);
        Set<String> str3 = linkedHashMap.keySet();
        System.out.println(str3);    //[d, e, b, c, a]

可以看到输出的key和插入的key顺序是一样的,在对某个key进行操作后,这个key会出现在链表的结尾。注意要实现LRU就要在构造函数设置LRU模式,不设置的话顺序就一直是插入的顺序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值