从源码看LinkedHashMap如何实现的排序

LinkedHashMap源码分析

 LinkedLinkedHashMap继承自HashMap,与HashMap不同的是,LinkedLinkedHashMap实现了HashMap所不具备的排序功能。所使用的方式是将HashMap的节点连接成双向链表结构,从而实现排序功能。
  LinkedHashMap具有两种排序方式:按照插入顺序排序和按照最近使用顺序排序。而最近使用顺序正好是LRU算法实现的关键,因此可以用LinkedHashMap轻松实现Lru缓存算法。

  下面为LinkedHashMap的双链表模型:

LinkedHashMap的双链表模型

LinkedHashMap的定义
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{

    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,并且拓展了HashMap的节点Node。增加了两个属性,分别是before和after。从名字就可以看出,这两个属性正是实现双向链表的关键。
  LinkedHashMap并未改变HashMap的排序规则,只是将其数据通过before和after串联成了一个双向链表而已。

但是在android中,为了能够适应早期的Android版本,将HashMap.Node命名成了LinkedHashMapEntry。其余的并没有什么改变。
	//截取的部分注释
	/**
    // BEGIN Android-changed
     * LinkedHashMapEntry should not be renamed. Specifically, for
     * source compatibility with earlier versions of Android, this
     * nested class must not be named "Entry". Otherwise, it would
     * hide Map.Entry which would break compilation of code like:
     *
     * LinkedHashMap.Entry<K, V> entry = map.entrySet().iterator.next()
     *
     * To compile, that code snippet's "LinkedHashMap.Entry" must
     * mean java.util.Map.Entry which is the compile time type of
     * entrySet()'s elements.
     // END Android-changed
     */
     
     /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

因此在LinkedHashMap中,实际中的存储方式如下图:

LinkedHashMap实际的存储模型

LinkedHashMap的一些属性
    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMapEntry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMapEntry<K,V> tail;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

  LinkedHashMap定义了两个引用head和tail,分别对应链表的头部和尾部。
  head又称为eldest,是当LinkedHashMap按照最近访问次序排序的话,head则对应最近访问次序最少的节点。
  accessOrder则对应排列方式,true:按照访问次序排序,false:按照插入次序排序

构造函数

LinkedHashMap一共有5个构造方法,内部对应HashMap的构造方法

    //空参数直接调用父类的构造函数,即HashMap的构造方法。
    //initialCapacity 默认16,loadFactory默认0.75
    //具体含义请看HashMap的源码
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
    
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

  由上述可以看到,LinkedHashMap认为排序方式为插入顺序排序,只有一种构造方法可以设置其排序方式。putMapEntries为HashMap中的实现,是将Map中的数据存储在HashMap中,详情可查看HashMap的源码。

gat方法的改变
    public V get(Object key) {
        Node<K,V> e;
        //直接调用HashMap的getNode方法获取数据
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //在访问到数据后,将调用afterNodeAccess方法将最近访问的节点移动到链尾tail
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

    //带默认值的get
    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;
   }

  当设置了按照访问次序进行排列的时候,每次访问数据都将会调用afterNodeAccess方法,将刚访问到的数据节点移动到链表的尾部tail。

afterNodeAccess 这个方法是HashMap中的方法,默认是空实现,在LinkedHashMap中被重写
    void afterNodeAccess(Node<K,V> e) { // move node to last
        //last用来记录未移动之前的尾部节点
        LinkedHashMap.Entry<K,V> last;
        //当按照访问次序排列,并且该访问节点不是尾部节点时开始移动
        if (accessOrder && (last = tail) != e) {
            //这里定义了3个变量,p代表当前访问节点,b和a对应before和after
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            
            p.after = null;
            //前一个节点为空则代表该节点为链首head节点,因此将head节点指向它的后一个节点
            //不为空则代表是中间节点,直接让它的前一个节点指向它的后一个节点
            if (b == null)
                head = a;
            else
                b.after = a;
            
            //同上,判断他是不是最后一个节点tail
            if (a != null)
                a.before = b;
            else
                last = b;
            
            //这种情况是第一次使用时没有节点,tail和head都指向null
            if (last == null)
                head = p;
            else {
                //将当前节点连接到最后一个节点
                p.before = last;
                last.after = p;
            }
            //并将当前节点设为尾指针tail
            tail = p;
            ++modCount;
        }
    }

  afterNodeAccess方法实现全是节点指针的指向的转换,并未涉及到结构的变换,这里也可以看出,LinkedHashMap是对HashMap的拓展而并非改变。仅是通过增加两个节点指针实现的排序。

在HashMap中,put一个新的数据时,若是不存在该Key,则会调用newNode方法生成一个新的Node进行插入。在LinkedHashMap中,则复写了该方法,是为了将节点转换成带before和after的节点,并且进行双向链表的维护。
newNode
    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);
        //维护双链表,将该节点移动到链尾tail
        linkNodeLast(p);
        return p;
    }
linkNodeLast 将节点链接到链表尾部
    //该方法将节点连接到链表的尾部tail
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        //链表为空的时候,将链首head也指向该节点
        if (last == null)
            head = p;
        //否则就将该节点连在原先链表的尾部
        else {
            p.before = last;
            last.after = p;
        }
    }

  上面说到,在HashMap中,若是put一个新的数据,并且该key不存在map中时,会调用newNode方法生成一个节点,并继而调用方法afterNodeInsertion,该方法在HashMap中是空实现,由LinkedHashMap具体实现。
  若是put的key已经在map中存在,则会替换value值,并调用afterNodeAccess方法。该方法在上面已经提过在HashMap中空实现,是由LinkedHashMap实现的,并且在get获取值的时候也会被调用,用于将访问节点移动至链尾tail.

afterNodeInsertion
    //该方法在HashMap中添加数据时会被调用,在HashMap中为空实现
    //在实际插入时参数才会为true,像clone等回调的该方法参数则为false
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        //removeEldestEntry方法用于判断是否将进行移除eldest节点
        //属于LinkedHashMap的方法,默认返回false,即不自动移除head(eldest)节点
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            //移除节点方法,属于HashMap的方法
            removeNode(hash(key), key, null, false, true);
        }
    }

  可以看出,afterNodeInsertion方法是用于在map中插入数据的时候调用的,默认为空实现,目的是为了让子类实现判断是否需要删除头结点head(eldest)。

该方法只有在一般情况向HashMap中插入数据的时候参数才会为true。而如clone和带map参数的构造方法等回调出的该方法,参数均为false。而put/putAll等方法回调而来的参数则是true。

  而子类LinkedHashMap在实现该方法的后,又将该方法拓展,设置了一个方法叫做removeEldestEntry,并默认返回false表示不自动删除。其子类可以通过重写这个方法来加入自动移除eldest节点的条件。(在Android的LruCache并没有重写该方法来实现自动移除eldest节点)

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

  在HashMap的removeNode方法中,移除节点后会进一步调用afterNodeRemoval方法,该方法也是空实现,具体由LinkedHashMap实现的

afterNodeRemoval
    //该方法是将移除的节点的引用消除,即清除before/after的引用
    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;
    }

  在Java8中,HashMap的实现是数组+链表+红黑树,即在链表超过一定长度的时候,就会将链表转换成红黑树,这里又涉及到节点的转换,因此也要维护双链表的前驱节点和后继节点。

  在转换的过程中会涉及到两个方法replacementTreeNode和replacementNode,树化/链表化过程中对节点的转换。这里回调是为了维护LinkedHashMap的双链表结构。

    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        LinkedHashMap.Entry<K,V> t =
            new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }
    
      TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }
    

这两个方法在这里被复写,最终都会调用transferLinks方法进行链表的维护

    //替换二者的before/after引用
    private void transferLinks(LinkedHashMap.Entry<K,V> src,
                               LinkedHashMap.Entry<K,V> dst) {
        LinkedHashMap.Entry<K,V> b = dst.before = src.before;
        LinkedHashMap.Entry<K,V> a = dst.after = src.after;
        if (b == null)
            head = dst;
        else
            b.after = dst;
        if (a == null)
            tail = dst;
        else
            a.before = dst;
    }
其他方法
    //判断是否存在某个value
    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;
    }
    
    //重新初始化数据,在clone和readObject时调用
    void reinitialize() {
        super.reinitialize();
        head = tail = null;
    }
    
    //清空map中的所有数据
    public void clear() {
        super.clear();
        head = tail = null;
    }
    

  另外,LinkedHashMap还重写HashMap的entrySet方法,用于实现按顺序遍历Map集合。在HashMap中是按照数组的的顺序->链表的顺序来进行遍历的。

    //可以看出,其替换了HashMap的EntrySet类。
    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
    }
LinkedEntrySet类
    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        ...
        //主要区别在这个方法,也是替换了iterator
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        ...
    }
继续追踪到 LinkedEntryIterator
    //这里倒是没有什么区别,都是调用LinkedHashIterator的nextNode方法
    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
nextNode是在抽象类LinkedHashIterator中实现的
    abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {
            return next != null;
        }

        //区别在这里
        //原HashMap是按照数组顺序遍历
        //这里是按照构建的双链表的次序进行遍历
        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

至此,LinkedHashMap的源码基本已经分析完了。

总结
  • LinkedHashMap 是继承于HashMap的,内部结构实现也是HashMap的实现(数组+链表+红黑树)。
  • LinkedHashMap给HashMap的节点包装了两个属性before/after,用于构建双向链表。它与HashMap中的单链表没有任何关系,只是拓展了两个引用来构建双向链表而已,在HashMap中的单链表的顺序是新插入的数据在链表尾部。
  • LinkedHashMap有两种排序方式,默认按照插入顺序排序,另一种是按照最近访问顺序排序
  • 内部有两个指针,head(eldest)和tail。Java8中,每次插入新数据都是插入到tail尾部,更新访问的时候也是将节点调整到尾部。
  • 在HashMap的增加(put等),删除(remove等),修改(put已经存在的key,链表转换成红黑树等),查询(get等)等方法中都设置或调用回调接口,用于子类实现。LinkedHashMap利用这些方法将内部节点维护成了双链表。
  • LinkedHashMap有一个方法removeEldestEntry,默认返回false,可用于设置自动删除head(eldest)节点,返回true则表示需要删除head节点。若是使用LinkedHashMap构建LruCache的话可以通过该方法设置相应的条件来实现。
  • Android中给LinkedHashMap的节点改变了名字并且加入了一个方法,叫做public Map.Entry<K, V> eldest() {return head; }。返回的是链表头指针,是为了LruCache的使用方便。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值