0
点赞
收藏
分享

微信扫一扫

Java集合(五)LinkedHashMap使用及源码分析

码农K 2022-03-20 阅读 45
java

文章目录

LinkedHashMap

一、LinkedHashMap简介

1.1 LinkedHashMap的继承关系

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

  图示:

  从继承关系上看,LinkedHashMap继承了HashMap的特性,并且在其基础上扩展了一些新特性(元素的有序性)。

  • 2、LinkedHashMap的底层结构
      说LinkedHashMap之前,先说HashMap。HashMap是无序的,也就是说,迭代HashMap所得到的元素顺序并不是它们最初添加到HashMap的顺序。为了保证迭代元素的顺序与存入容器的顺序一致,诞生了HashMap的子类------LinkedHashMap。
      由于LinkedHashMap是HashMap的子类,所以LinkedHashMap自然会拥有HashMap的所有特性。比如,LinkedHashMap也最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。
      本质上,HashMap和双向链表合二为一即是LinkedHashMap。更准确地说,它是一个将所有Entry节点链入一个双向链表双向链表的HashMap。

  关键的地方来了,在HashMap有一些空方法,比如:

    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

  LinkedHashMap重写了这些方法,这些方法被用来保持列表的有序。
  关于LinkedHashMap和HashMap的差异,借用网上两张图来说明。

  • HashMap的结构:
  • LinkedHashMap的结构:

1.2 LinkedHashMap特点

  • 1、由于继承HashMap类,所以默认初始容量是16,加载因子是0.75。
  • 2、线程不安全。
  • 3、具有fail-fast的特征。
  • 4、底层使用双向链表,可以保存元素的插入顺序,顺序有两种方式:一种是按照插入顺序排序,一种按照访问做排序。默认以插入顺序排序。
  • 5、key和value允许为null,key重复时,新value覆盖旧value。
  • 6、可以用来实现LRU算法。
  • 7、LinkedHashMap与HashMap的存取数据操作基本是一致的,只是增加了双向链表保证数据的有序性。
  • 8、LinkedHashMap继承HashMap,基于HashMap+双向链表实现。(HashMap是数组+链表+红黑树实现的)。

二、LinkedHashMap常见方法

  • 1、构造具有默认初始容量(16)和负载因子(0.75)的LinkedHashMap实例
	public LinkedHashMap()
  • 2、构造具有指定初始容量和默认负载因子(0.75)的LinkedHashMap实例
	public LinkedHashMap(int initialCapacity)
  • 3、构造具有指定初始容量和负载因子的LinkedHashMap实例
	public LinkedHashMap(int initialCapacity, float loadFactor)
  • 4、构造一个空的 LinkedHashMap实例,具有指定的初始容量,负载因子和订购模式(accessOrder)。accessOrder为false时,基于插入顺序;accessOrder为true时,基于访问顺序
	public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
  • 5、清空元素
	public void clear()
  • 6、判断LinkedHashMap是否包含某个value
	public boolean containsValue(Object value)
  • 7、返回LinkedHashMap中的Entry形成的Set
	public Set<Map.Entry<K,V>> entrySet()
  • 8、对LinkedHashMap中的每个元素执行特定操作
	public void forEach(BiConsumer<? super K, ? super V> action)
  • 9、获取某个key对应的value
	public V get(Object key)
  • 10、获取某个key对应的value,该key不存在时,返回默认值
	public V getOrDefault(Object key, V defaultValue)
  • 11、返回LinkedHashMap中的key形成的Set
	public Set<K> keySet()
  • 12、返回value形成的集合
	public Collection<V> values()

三、LinkedHashMap源码解析

3.1 Entry

  LinkedHashMap中存储的节点:

    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中的节点Entry,相比于HashMap中的节点Node,多了指向前后位置的节点before和after,用于维护双向链表(即保持元素有序)。
  HashMap与LinkedHashMap的Entry结构示意图:

  LinkedHashMap中的变量:

	private static final long serialVersionUID = 3801124242820219131L;
	//头结点
	transient LinkedHashMap.Entry<K,V> head;
	//尾节点
	transient LinkedHashMap.Entry<K,V> tail;
	//这个变量决定链表元素的存储方式,false按照存储顺序存储,true表示按照访问顺序
	//存储(将最近访问的元素移动到尾部),该变量和LRU算法相关 
	final boolean accessOrder;

3.2 构造方法

  • 1、LinkedHashMap(int initialCapacity, float loadFactor)
    public LinkedHashMap(int initialCapacity, float loadFactor) {
    	//指定初始容量与负载因子
        super(initialCapacity, loadFactor);
        //默认访问顺序标识为false,表示按照存储顺序存储
        accessOrder = false;
    }
  • 2、LinkedHashMap(int initialCapacity)
    public LinkedHashMap(int initialCapacity) {
    	//指定初始容量,负载因子为0.75
        super(initialCapacity);
        //默认访问顺序标识为false,表示按照存储顺序存储
        accessOrder = false;
    }
  • 3、LinkedHashMap()
    public LinkedHashMap() {
    	//默认初始容量(16)和负载因子(0.75)
        super();
        //默认访问顺序标识为false,表示按照存储顺序存储
        accessOrder = false;
    }
  • 5、LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        //指定初始容量与负载因子
        super(initialCapacity, loadFactor);
        //指定默认访问顺序标识
        this.accessOrder = accessOrder;
    }

3.3 是否包含某个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;
    }

3.4 获取元素

  • 1、get(Object key)
    public V get(Object key) {
        Node<K,V> e;
        //第一步是直接使用Hashmap中的函数getNode方法,获取value,如果为null,返回null
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
        	//将添加的元素移动到链表的尾端
            afterNodeAccess(e);
        return e.value;
    }

	//getNode方法来自于其父类HashMap
	final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            //如果此key对应的hash桶不为空执行以下
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            //如果需要查询的是此哈希桶的第一个元素,直接返回
                return first;
            if ((e = first.next) != null) {
                //如果是红黑树的结果,就需要按照红黑树的方式查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
 
                do {
                    //遍历整个链表,找到key对应的value并且返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

	//将访问的这个元素移动到双向链表的尾端,并且将他的后面的元素向前移动
	void afterNodeAccess(Node<K,V> e) {
        //last为原链表的尾节点
        LinkedHashMapEntry<K,V> last;
        //如果accessOrder为true,并且尾端元素不是需要访问的元素
        if (accessOrder && (last = tail) != e) {
            //将节点e强制转换成linkedHashMapEntry,b为这个节点的前一个
            //节点,a为它的后一个节点
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            
            //p为最后一个元素,那么他的后置节点必定是空
            p.after = null;
            //b为e的前置元素,如果b为空,说明此元素必定是链表的第一个元素,更新之后
            //链表的头结点已经变成尾节点,那么原链表的第二个节点就要变为头结点
            if (b == null)
                head = a;
            else
                //如果b不是空,那么b的后置节点就由p变为p的后置节点
                b.after = a;
 
            //如果p的后置节点不为空,那么更新后置节点a的前置节点为b
            if (a != null)
                a.before = b;
            else
            //如果p的后置节点为空,那么p就是尾节点,那么更新last的节点为p的前置节点
                last = b;
            //如果原来的尾节点为空,那么原链表就只有一个元素
            if (last == null)
                head = p;
            else {
            //更新当前节点p的前置节点为 原尾节点last, last的后置节点是p
                p.before = last;
                last.after = p;
            }
 
            //p为最新的尾节点
            tail = p;
            ++modCount;
        }
    }

  这里需要注意两点,一是调用次函数之后,访问的这个元素会移动到双向链表的尾端,二是在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询(get)访问数据,也会导致fail-fast,因为迭代的顺序已经改变。

  • 2、getOrDefault
	//实现与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;
   }

3.5 清空LinkedHashMap

    public void clear() {
        super.clear();
        //将头尾节点都置为null
        head = tail = null;
    }

	//HashMap的clear方法
	public void clear() {
        Node<K,V>[] tab;
        modCount++;
        //数组中所有元素都置为null
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

3.6 获取key组成的Set

public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            //在这里linkedHashMap重写了此函数。
            ks = new LinkedKeySet();
            keySet = ks;
        }
        return ks;
    }
 
    final class LinkedKeySet extends AbstractSet<K> {
        //链表元素个数
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
 
        //遍历的目的是获取Iterator
        public final Iterator<K> iterator() {
            return new LinkedKeyIterator();
        }
        //这个直接使用的hashmap的containsKey函数,最终使用的是getNode,前文已经讲过就不在强调
        public final boolean contains(Object o) { return containsKey(o); }
 
        //remove最后也是调用了removeNode,前文已经讲过。就不在强调
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
      
    }

    final class LinkedKeyIterator extends LinkedHashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().getKey(); }
    }

3.7 获取value组成的Collection

    public Collection<V> values() {
        Collection<V> vs;
        return (vs = values) == null ? (values = new LinkedValues()) : vs;
    }

    final class LinkedValues extends AbstractCollection<V> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator<V> iterator() {
            return new LinkedValueIterator();
        }
        //linkedHashMap重写了contains函数
        public final boolean contains(Object o) { return containsValue(o); }
		//省略了一些方法
    }

    final class LinkedValueIterator extends LinkedHashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

3.8 获取entry组成的Set

 public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
    }
 
    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        
        //之后的遍历都是通过Iterator来进行
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            //通过getnode方法来获取节点
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
       
    }

    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

3.9 LinkedHashMap中的核心迭代器

 abstract class LinkedHashIterator {
        //下一个节点
        LinkedHashMapEntry<K,V> next;
        //当前节点
        LinkedHashMapEntry<K,V> current;
        int expectedModCount;
 
        LinkedHashIterator() {
            //初始化的时候将next指向双向链表的头结点
            next = head;
            expectedModCount = modCount;
            //current为空
            current = null;
        }
 
        public final boolean hasNext() {
            return next != null;
        }
 
        //nextNode方法就是我们用到的next方法,
        //迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出
        final LinkedHashMapEntry<K,V> nextNode() {
        	//记录要返回的e
            LinkedHashMapEntry<K,V> e = next;
            //fail-fast判断
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //如果返回的是空,则抛出异常
            if (e == null)
                throw new NoSuchElementException();
            //更新当前节点为e
            current = e;
            //更新下一个节点是e的后置节点
            next = e.after;
            return e;
        }
 
        //删除方法,就直接使用了hashmap的remove
        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只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像LinkedList那样进行双向访问。
  在新增节点时,已经维护了元素之间的插入顺序了,所以在迭代访问时只需要不断的访问当前节点的下一个节点即可。

3.10 添加元素

  由于LinkedHashMap是HashMap的子类,添加元素是通过调用父类的putVal来完成的。在HashMap的putVal方法中,有调用newNode(hash, key, value, 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<K,V>(hash, key, value, e);
        // 作用:将新建的节点添加到维护的双向链表上去
        // 方式:往链表的尾部进行添加
        linkNodeLast(p);
        return p;
    }

    //在双向链表的尾部添加新建的节点
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
       // p为新的需要追加的结点
        tail = p;
        // 如果last为null.则表示现在链表为空。新new出来的p元素就是链表的头结点
        if (last == null)
            head = p;
        // 否则就是链表中已存在结点的情况:往尾部添加即可
        else {
        	// 把新追加p的结点的前驱结点设置之前的尾部结点
        	// 把之前的尾部结点的后驱结点设为新追加的p结点
            p.before = last;
            last.after = p;
        }
	}

  可以看出LinkedHashMap的在新建一个结点的时候,做了两件事:

  结合构造方法来看的话:LinkedHashMap初始化时,accessOrder为false,就会按照插入顺序提供访问,插入方法使用的是父类HashMap的put方法,不过覆写了put方法,执行中调用的是newNode/newTreeNode和afterNodeAccess 方法。
  LinkedHashMap通过新增头节点、尾节点,给每个节点增加before、after 属性。每次新增时,都把节点追加到尾节点,这样就可以保证新增节点是按照顺序插入到链表中的。

  LinkedHashMap的没有自己的put方法的实现,而是使用父类HashMap的put方法:在正常新增之后,调用afterNodeAccess(e)和 afterNodeInsertion(evict)让LinkedHashMap自己做后续处理:

	//回调函数,新节点插入之后回调 , 根据evict判断是否需要删除最老插入的节点。
    void afterNodeInsertion(boolean evict) {
        LinkedHashMap.Entry<K,V> first;
        //LinkedHashMap 默认返回false 则不删除节点
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    //移动访问的节点到链表末端(该方法在get小节已详细介绍过)
	void afterNodeAccess(Node<K,V> e) {
		//省略代码....
    }

    //LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
	}

3.11 删除元素

  LinkedHashMap删除元素时,也是通过调用父类的方法来实现的。LinkedHashMap在删除元素时,会调用到自己重写的afterNodeRemoval方法:

 	//在删除节点e时,同步将e从双向链表上删除
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //待删除节点 p 的前置后置节点都置空
        p.before = p.after = null;
        //如果前置节点是null,则现在的头结点应该是后置节点a
        if (b == null)
            head = a;
        else//否则将前置节点b的后置节点指向a
            b.after = a;
        //同理如果后置节点时null ,则尾节点应是b
        if (a == null)
            tail = b;
        else//否则更新后置节点a的前置节点为b
            a.before = b;
    }
举报

相关推荐

0 条评论