LinkedHashMap源码查看

一、背景

    LinkedHashMap是基层HashMap的一个子类,所以很多方法都是用的HashMap的方法,尚未了结HashMap的同学可以去看下我的上一篇博客,在本篇博客中涉及到HashMap的源码方法就不不粘贴了,同样本篇会讲解下java7.和java8的代码

java7的版本号为1.7.0_04,java8的版本号为1.8.0_162

二、问题

LinkedHashMap的数据结构

三、构造函数

1.java7

//循环链表的头结点
private transient Entry<K,V> header;

// 双向链表中元素排序规则的标志位
// accessOrder为false,表示按插入顺序排序
// accessOrder为true,表示按访问顺序排序
private final boolean accessOrder;

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(m);
	accessOrder = false;
}

public LinkedHashMap(int initialCapacity,
					 float loadFactor,
					 boolean accessOrder) {
	super(initialCapacity, loadFactor);
	this.accessOrder = accessOrder;
}

//继承的entry对象,添加了一个before,after
private static class Entry<K,V> extends HashMap.Entry<K,V> {
	// These fields comprise the doubly linked list used for iteration.
	Entry<K,V> before, after;

	Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
		super(hash, key, value, next);
	}
}	
	

2、java8

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;

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

可以看到无论是java7还是java8,构造函数和HashMap相比,就是增加了一个accessOrder参数。用于控制迭代时的节点顺序。然后就是调用hashmap的构造函数

四、put方法

1.java7

在java8中LinkedHashMap并没有重写任何put方法。但是重写了addEntry方法,该方法在putForNullKey()和put方法中调用,在map的某个下标链表增加一个entry'节点

void addEntry(int hash, K key, V value, int bucketIndex) {
	createEntry(hash, key, value, bucketIndex);

	// Remove eldest entry if instructed, else grow capacity if appropriate
	Entry<K,V> eldest = header.after;
	if (removeEldestEntry(eldest)) {
		removeEntryForKey(eldest.key);
	} else {
		if (size >= threshold)
			resize(2 * table.length);
	}
}


void createEntry(int hash, K key, V value, int bucketIndex) {
	HashMap.Entry<K,V> old = table[bucketIndex];
	Entry<K,V> e = new Entry<>(hash, key, value, old);
	table[bucketIndex] = e;
	e.addBefore(header);
	size++;
}

private void addBefore(Entry<K,V> existingEntry) {
	after  = existingEntry;
	before = existingEntry.before;
	before.after = this;
	after.before = this;
}

通过上面的代码可以看到,新增一个enrty时,将其添加到双向链表的尾部,

实现方式是在addBefor中,始终获取header节点,header节点的before节点(也就是原先的尾节点)的after指向当前节点,header节点的before指向当前节点

同时还在存值key则覆盖旧值时,重写了recordAccess方法,

A)recordAccess方法

void recordAccess(HashMap<K,V> m) {
	LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
	if (lm.accessOrder) {
		lm.modCount++;
		//在双向链表移除当前元素的前后指针指向
        remove();
		//将当前元素添加到双向链表的尾部
        addBefore(lm.header);
	}
}

private void remove() {
    //将当前节点的后一个元素指向前一个元素的后一个元素
	before.after = after;
    //将当前节点的前一个元素指向给后一个节点的前一个节点
	after.before = before;
}

2.java8

在java8中LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法

newNode()会在HashMapputVal()方法里被调用,putVal()方法会在批量插入数据putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入单个数据public V put(K key, V value)时被调用。

LinkedHashMap重写了newNode(),在每次构建新节点时,通过linkNodeLast(p);将新节点链接在内部双向链表的尾部。

//在构建新节点时,构建的是`LinkedHashMap.Entry` 不再是`Node`.
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;
	}
}

在hashmap的put方法中,如果要添加的key存在,则覆盖旧值,然后调用afterNodeAccess 函数,

不存在旧值,则判断是否需要扩容,然后调用afterNodeInsertion函数。

A)afterNodeAccess函数

afterNodeAccess函数会将当前被访问到的节点e,移动至内部的双向链表的尾部。

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;//原尾节点
        //如果accessOrder 是true ,且原尾节点不等于e
        if (accessOrder && (last = tail) != e) {
            //节点e强转成双向链表节点p
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //p现在是尾节点, 后置节点一定是null
            p.after = null;
            //如果p的前置节点是null,则p以前是头结点,所以更新现在的头结点是p的后置节点a
            if (b == null)
                head = a;
            else//否则更新p的前直接点b的后置节点为 a
                b.after = a;
            //如果p的后置节点不是null,则更新后置节点a的前置节点为b
            if (a != null)
                a.before = b;
            else//如果原本p的后置节点是null,则p就是尾节点。 此时 更新last的引用为 p的前置节点b
                last = b;
            if (last == null) //原本尾节点是null  则,链表中就一个节点
                head = p;
            else {//否则 更新 当前节点p的前置节点为 原尾节点last, last的后置节点是p
                p.before = last;
                last.after = p;
            }
            //尾节点的引用赋值成p
            tail = p;
            //修改modCount。
            ++modCount;
        }
    }

值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。

B)afterNodeInsertion函数

 //回调函数,新节点插入之后回调 , 根据evict 和   判断是否需要删除最老插入的节点。如果实现LruCache会用到这个方法。
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        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);
        }
    }
    //LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

看到这里我们已经回答前面提到的问题了

LinkedHashMap的数据结构是怎样的

LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表或红黑树组成(java7除去红黑树)。而LinkedHashMap 在上面结构的基础上维持了一个双向链表。插入节点时,将节点追加到双向链表尾部,从而实现按照插入顺序的有序访问。也可以在初始化LinkedHashMap对象时设定为按照访问顺序排序,此时每当访问一个节点,afternodeaccess方法就会将该节点放到双向链表的尾部,从而实现按照访问顺序的有序遍历访问。其结构可能如下图:

五、remove方法

1.java7

在java7中并没有重写remove方法,但是重写了recordRemoval,该方法在remove方法中调用,用途就是将节点删除时,同步将该节点从双向链表上删除。
void recordRemoval(HashMap<K,V> m) {
	remove();
}

private void remove() {
    //将当前节点的后一个元素指向前一个元素的后一个元素
	before.after = after;
    //将当前节点的前一个元素指向给后一个节点的前一个节点
	after.before = before;
}

2.java8

在java8中LinkedHashMap也没有重写remove()方法,因为它的删除逻辑和HashMap并无区别。但它重写了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;
}

六、get方法

1、java7

java7重写了get方法,getEntry为hashmap中的方法,

public V get(Object key) {
	Entry<K,V> e = (Entry<K,V>)getEntry(key);
	if (e == null)
		return null;
	e.recordAccess(this);
	return e.value;
}

获取到元素之后,调用recordAccess方法将当前被访问到的节点e,移动至内部的双向链表的尾部。前面已经讲解

2、java8

LinkedHashMap重写了get()和getOrDefault()方法

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

对比HashMap中的实现,LinkedHashMap只是增加了在成员变量(构造函数时赋值)accessOrder为true的情况下,要去回调void afterNodeAccess(Node<K,V> e)函数。该函数会将当前被访问到的节点e,移动至内部的双向链表的尾部。前面已经讲解

七、containsValue方法

1.java7

public boolean containsValue(Object value) {
	// Overridden to take advantage of faster iterator
	if (value==null) {
		for (Entry e = header.after; e != header; e = e.after)
			if (e.value==null)
				return true;
	} else {
		for (Entry e = header.after; e != header; e = e.after)
			if (value.equals(e.value))
				return true;
	}
	return false;
}

2.java8

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

java7和java8重写了containsValue方法,原因是在HashMap的实现中,HashMap查找value时用了两个for循环,即使数组中的某一下标没有对应的链表,也要去查找,而LinkedHashMap查找value时,是通过双向链表来查找的,链表中的每一个节点都是有效地,而不用再去查找整个哈希表,更为高效。

那为什么LinkedHashMap没有重写containsKey(Object)方法呢?

因为HashMap的containsKey(Object)方法已经很高效了,HashMap的containsKey(Object)方法是去key对应的数组链表中去查找,其节点个数可能远远小于双向链表的节点个数,所以LinkedHashMap采用HashMap实现的containsKey(Object)方法就可以了。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值