Java LinkedHashMap的实现原理(Android10&JDK1.8)

LinkedHashMap是一个实现了哈希表和链表的映射类。其在Java中的类组织结构如下:
在这里插入图片描述

LinkedHashMapHashMap的子类,继承了HashMap所有的可选操作,所以,在了解LinkedHashMap之前必须先知道HashMap是怎么一回事

HashMap底层实现原理(上)
HashMap底层实现原理(下)
阅读HashMap源码时你可能会有这些疑问



LinkedHashMapHashMap的不同之处在于,它维护一个贯穿其所有条目的双链接列表,有可预测的迭代顺序,这个顺序可用是键插入到映射中的顺序(默认),或访问的顺序。

先看一下在LinkedHashMap中一个普通元素的节点子类:

/**
 * HashMap。普通LinkedHashMap条目的节点子类。
 */
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);
	}
}

/**
 * 双链表的头(最年长的)。
 */
transient LinkedHashMapEntry<K,V> head;

/**
 * 双链表的尾部(最年轻的)。
 */
transient LinkedHashMapEntry<K,V> tail;

可以看到在LinkedHashMap中除了继承了HashMap的数组(Node<K,V>[] table)之外,其节点实现也由单向链表变成了一个双向的链表结构。

  • HashMap中的单向链表仅用于拉链法解决哈希冲突,每个节点对应一个链表,不同节点之间的链表没有连接关系。
  • LinkedHashMap中的双向链表是不仅要起到HashMap中链表的作用,还要负责连接每一个元素,具元素之间具有完整的连接关系。

LinkedHashMap提供了一个特殊的构造函数来创建一个链表哈希映射:LinkedHashMap(int,float,boolean),当accessOrdertrue时,其迭代顺序是最后访问它的条目的顺序。这种映射非常适合于构建LRU缓存(Android LruCache实现原理)。

/**
 * 构造一个空的LinkedHashMap实例,该实例具有指定的初始容量、负载因子和排序模式。
 * 
 * @param  accessOrder     顺序模式为:true为访问顺序,为false为插入顺序
 */
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
	super(initialCapacity, loadFactor);
	this.accessOrder = accessOrder;
}
插入顺序

LinkedHashMap并没有实现自己的put方法,说明他的put操作还是沿用了HashMap的实现。不过,LinkedHashMap却重写了HashMapput操作过程中调用过的newNode(int , K , V , Node)方法。也就是说,LinkedHashMap的数据依旧是存储在数组中,且通过hash值计算出其在数组中的位置,同样也是采用了拉链法和红黑树化来处理哈希冲突。不同之处便在其元素节点之间在创建时会构成一个链式的存储结构。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
	LinkedHashMapEntry<K,V> p =
		new LinkedHashMapEntry<K,V>(hash, key, value, e);
	linkNodeLast(p);
	return p;
}

/**
 * 链接在列表的末尾
 */
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
	LinkedHashMapEntry<K,V> last = tail;
	tail = p;
	if (last == null)
		head = p;
	else {
		p.before = last;
		last.after = p;
	}
}

插入的顺序很容易理解,由linkNodeLast(LinkedHashMapEntry<K,V> p)方法可以得出结论:每插入一条key不存在的数据,该数据将被连接在全部数据元素的链表尾部。

那么如果插入一条key已经存在的数据呢?我们顺着HashMap#putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法的最后可以看到如下代码:

if (e != null) { // existing mapping for key
	V oldValue = e.value;
	if (!onlyIfAbsent || oldValue == null)
		e.value = value;
	afterNodeAccess(e);
	return oldValue;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }

afterNodeAccessHashMap中是一个空方法,注释中明确说明这个回调是给LinkedHashMap用的。这个方法顾名思义:Node节点被访问之后。那么我们从LinkedHashMap#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;
}

LinkedHashMapget的实现大体相当于HashMap,只有当accessOrdertrue时会通过afterNodeAccess(e)对链表做些特殊的处理。这个方法的作用是将当前正在访问的节点e移动到链表的尾部。

void afterNodeAccess(Node<K,V> e) { // move node to last
	LinkedHashMapEntry<K,V> last;
	if (accessOrder && (last = tail) != e) {
		LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
		p.after = null;//因为p要被移动到链表尾部,所以他向后的指针应指向null
		if (b == null)
			head = a;//如果p位于链表头部,让p的下一个节点做头部
		else
			b.after = a;//如果p不位于链表头部,让p的上一个节点(b)向后的指针指向p的下一个节点(a)
		if (a != null)
			a.before = b;//如果p不位于链表尾部,让p的下一个节点(a)向前的指针指向e的上一个节点(a)
		else
			last = b;//如果e位于链表尾部,让e的上一个节点做尾部
			
		//至此为止,节点e已经被从链表中单独剥离出来了。
		if (last == null)
			head = p;//last == null意味着此时链表长度为1,即p既是头也是尾
		else {//将p连接到链表尾部
			p.before = last;
			last.after = p;
		}
		tail = p;
		++modCount;
	}
}

到这里关于LinkedHashMap的顺序模式可以总结为:

  • 插入顺序的模式下,LinkedHashMap每次插入元素时(不论key是否存在)都按照插入的先后顺序排列。
  • 访问顺序的模式下,LinkedHashMap每次访问元素时(get或put都视为访问),将访问的元素从原位置移动到链表的尾部。
遍历

最后再来看LinkedHashMap的遍历。其方式通常有如下两种用法:

// forEach遍历
map.forEach((key, value) -> {
	...
});
// 迭代器遍历
for (String key : map.keySet()) {
	String value = map.get(key);
}

不同于HashMapHashMap在进行遍历时是对数组Node[]直接进行遍历,所以无法保证插入的顺序和遍历的顺序一致。而LinkedHashMap是对其内部链表从头至尾进行遍历,进而也就能分别实现按照插入顺序或访问顺序输出遍历结果。

forEachjava8之后出现的,实现比较简短。

public void forEach(BiConsumer<? super K, ? super V> action) {
	if (action == null)
		throw new NullPointerException();
	int mc = modCount;
	// Android-changed: Detect changes to modCount early.
	for (LinkedHashMapEntry<K,V> e = head; modCount == mc && e != null; e = e.after)
		action.accept(e.key, e.value);
	if (modCount != mc)
		throw new ConcurrentModificationException();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值