JAVA基础(九)LinkedHashMap源码解析

本文参考了,部分图片也来自此博客:https://www.imooc.com/article/22931

LinkedHashMap

概述

LinkedHashMap继承自HashMap,它和HashMap基本相同,仅多了维护双向链表的代码。在LinkedHashMap中通过维护双向链表来解决HashMap的无序性问题。

数据结构

在HashMap的数组+链表+红黑树的基础上多了双向链表
在这里插入图片描述

源码分析

Entry类

Entry继承了HashMap中的Node类,仅仅多了两个属性before和after用来维护双向链表。

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);
        }
    }

添加结点

HashMap中的put()

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

HashMap中的putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
                   
        {.........}
        
        //当table中索引位没有结点时创建新结点放入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        
        	//当key相同时覆盖
            {.........}

            //当结点时红黑树结点时添加TreeNode,putTreeVal添加时调用了newTreeNode()
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            
				//循环遍历链表
            	{
            	//链表尾部加入新节点
               if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //长度大于等于7转换成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    
               		//链表中发现相同的key覆盖
                    {........}
                    
                  }
            }
            
       
        //判断是否需要扩容,modCount++
      	{...........}
      	
        return null;
    }

LinkedHashMap中重写的newNode()和newTreeNode()

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    	//创建LinkedHashMap重写的Entry类
        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;
        //将尾部指针指向新添加的Entry  p
        tail = p;
        if (last == null)
            head = p;
        else {
        	//p的前驱指向之前的尾结点
            p.before = last;
            //之前的尾节点的后继指向p
            last.after = p;
        }
    }
	
	//红黑树结点的双向链表维护和上述的雷同,不赘述
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }

简单总结一下LinkedhashMap添加结点的步骤

步骤
1调用父类的put函数
2put函数中创建新节点的函数newNode和newTreeNode被重写,多态的特性使得调用了LinkedHashMap重写的创建方式
3在LinkedHashMap重写的函数中创建结点的同时维护了双向链表的关系

获取结点

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
       
        //accessOrder属性在其中一个构造函数中可以指定
        if (accessOrder)
			//此方法是将key对应的结点加到链表尾部(不改动实际的位置,只是改变了链表中的前驱后继关系,逻辑位置)
            afterNodeAccess(e);
        return e.value;
    }

删除结点

HashMap中的remove()

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

HashMap中的removeNode()

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;

			//根据key在table中寻找对应的结点node
			{...........}
			
			//从链表或者红黑树中移除node,并维护链表或红黑树关系
			{...........}
			
			//执行完以上操作后调用的回调函数,在LinkedHashMap中重写
            afterNodeRemoval(node);
    }

LinkedHashMap中重写的afterNodeRemoval()函数,用于维护remove后双向链表的关系

    void afterNodeRemoval(Node<K,V> e) { // unlink
    	//向下转型,转成LinkedHashMap重写的Entry
        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;
    }

遍历分析

在概述中我们知道LinkedHashMap的遍历是有序的,而HashMap是无序的,分析一下原因。

LinkedHashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("源码",1);
        linkedHashMap.put("分析",2);
        linkedHashMap.put("博客",3);
        Set<Object> keySet = linkedHashMap.keySet();
        for (Object obj : keySet) {
            System.out.print(obj);
        }

        System.out.println();

        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("源码", 1);
        hashMap.put("分析", 2);
        hashMap.put("博客", 3);
        Set<Object> keySet2 = hashMap.keySet();
        for (Object obj : keySet2) {
            System.out.print(obj);
        }

以上demo的输出为

源码分析博客
分析博客源码

Map的遍历本质上是先遍历key再通过Key去获得value,返回的keySet的遍历方式有迭代器和for循环,增强for在编译后也是转换成迭代器实现,所以只需要分析LinkedHashMap的迭代器是如何实现的就可以知道它为何能有序输出了。(EntrySet也是同理,keySet只是在最后多调用了getKey())

Iterator iterator = linkedHashMap.keySet().iterator();

一步步分析调用,有点长但是不难

public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
        	//新建LinkedKeySet对象
            ks = new LinkedKeySet();
            keySet = ks;
        }
        return ks;
    }

final class LinkedKeySet extends AbstractSet<K> {
        public final Iterator<K> iterator() {
        	//新建LinkedKeyIterator对象
            return new LinkedKeyIterator();
        }
 }

//继承了LinkedHashIterator
final class LinkedKeyIterator extends LinkedHashIterator
        implements Iterator<K> {
       
        public final K next() { 
        	//调用父类的nextNode(在下面)获得node再返回Key
        	return nextNode().getKey(); 
        }
    }

    abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
        	//初始化时从双向链表头开始读
            next = head;
            //fail-fast机制
            expectedModCount = modCount;
            current = null;
       }
		
		public final boolean hasNext() {return next != null;}

        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;
        }
 }

简单总结一下步骤,并和HashMap进行对比更加直观。

步骤LinkedHashMapHashMap
1获取keySet对象同左
2获取keySet实现的迭代器同左
3迭代器初始化next=双向链表头,获得next时获取的是后继迭代器初始化时获得table[ ],next()是按照table的索引遍历

总结

LinkedHashMap和HashMap有何不同?

  1. 数据结构上LinkedHashMap多了一条双向链表
  2. 迭代器的实现上不同,LinkedHashMap是从双向链表头开始遍历,HashMap是按照table[ ]的索引开始读。
  3. 性能上HashMap比LinkedHashMap要高,因为LinkedHashMap需要比HashMap多维护一条双向链表的开销

为什么LinkedHashMap能有序遍历?

因为LinkedHashMap在对结点进行操作时会多维护一条双向链表,链表的顺序就是添加的顺序,在遍历时迭代器是从链表头开始遍历,所以它是有序的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值