Java LinkedHashMap工作原理及实现

时间:2021-05-20 16:46:41

Java LinkedHashMap工作原理及实现

1. 概述

在理解了#7 介绍的HashMap后,我们来学习LinkedHashMap的工作原理及实现。首先还是类似的,我们写一个简单的LinkedHashMap的程序:

1
2
3
4
5
6
7
8
9
10
11
12
LinkedHashMap<String, Integer> lmap = new LinkedHashMap<String, Integer>();
lmap.put( "语文" , 1 );
lmap.put( "数学" , 2 );
lmap.put( "英语" , 3 );
lmap.put( "历史" , 4 );
lmap.put( "政治" , 5 );
lmap.put( "地理" , 6 );
lmap.put( "生物" , 7 );
lmap.put( "化学" , 8 );
for (Entry<String, Integer> entry : lmap.entrySet()) {
     System.out.println(entry.getKey() + ": " + entry.getValue());
}

运行结果是:

1
2
3
4
5
6
7
8
语文: 1
数学: 2
英语: 3
历史: 4
政治: 5
地理: 6
生物: 7
化学: 8

我们可以观察到,和HashMap的运行结果不同,LinkedHashMap的迭代输出的结果保持了插入顺序。是什么样的结构使得LinkedHashMap具有如此特性呢?我们还是一样的看看LinkedHashMap的内部结构,对它有一个感性的认识:
Java LinkedHashMap工作原理及实现

没错,正如官方文档所说:

Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked listrunning through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).

LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。

2. 三个重点实现的函数

在HashMap中提到了下面的定义:

1
2
3
4
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion( boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

LinkedHashMap继承于HashMap,因此也重新实现了这3个函数,顾名思义这三个函数的作用分别是:节点访问后、节点插入后、节点移除后做一些事情。

afterNodeAccess函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void afterNodeAccess(Node<K,V> e) { // move node to last
     LinkedHashMap.Entry<K,V> last;
     // 如果定义了accessOrder,那么就保证最近访问节点放到最后
     if (accessOrder && (last = tail) != e) {
         LinkedHashMap.Entry<K,V> p =
             (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
         p.after = null ;
         if (b == null )
             head = a;
         else
             b.after = a;
         if (a != null )
             a.before = b;
         else
             last = b;
         if (last == null )
             head = p;
         else {
             p.before = last;
             last.after = p;
         }
         tail = p;
         ++modCount;
     }
}

就是说在进行put之后就算是对节点的访问了,那么这个时候就会更新链表,把最近访问的放到最后,保证链表。

afterNodeInsertion函数

1
2
3
4
5
6
7
8
void afterNodeInsertion( boolean evict) { // possibly remove eldest
     LinkedHashMap.Entry<K,V> first;
     // 如果定义了移除规则,则执行相应的溢出
     if (evict && (first = head) != null && removeEldestEntry(first)) {
         K key = first.key;
         removeNode(hash(key), key, null , false , true );
     }
}

如果用户定义了removeEldestEntry的规则,那么便可以执行相应的移除操作。

afterNodeRemoval函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

这个函数是在移除节点后调用的,就是将节点从双向链表中删除。

我们从上面3个函数看出来,基本上都是为了保证双向链表中的节点次序或者双向链表容量所做的一些额外的事情,目的就是保持双向链表中节点的顺序要从eldest到youngest。

3. put和get函数

put函数在LinkedHashMap中未重新实现,只是实现了afterNodeAccessafterNodeInsertion两个回调函数。get函数则重新实现并加入了afterNodeAccess来保证访问顺序,下面是get函数的具体实现:

1
2
3
4
5
6
7
8
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;
}

值得注意的是,在accessOrder模式下,只要执行get或者put等操作的时候,就会产生structural modification。官方文档是这么描述的:

A structural modification is any operation that adds or deletes one or more mappings or, in the case of access-ordered linked hash maps, affects iteration order. In insertion-ordered linked hash maps, merely changing the value associated with a key that is already contained in the map is not a structural modification. In access-ordered linked hash maps, merely querying the map with get is a structural modification.

不要犯了像ConcurrentModificationException with LinkedHashMap类似的问题。

总之,LinkedHashMap不愧是HashMap的儿子,和老子太像了,当然,青出于蓝而胜于蓝,LinkedHashMap的其他的操作也基本上都是为了维护好那个具有访问顺序的双向链表。:-)

 

HashMap与LinkedHashMap 区别

java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类,分别是HashMap Hashtable LinkedHashMap 和TreeMap.

Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。
Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。

LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

一般情况下,我们用的最多的是HashMap,在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.

HashMap是一个最常用的Map,它根据键的hashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为NULL,允许多条记录的值为NULL。

HashMap不支持线程同步,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致性。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。

Hashtable与HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtable在写入时会比较慢。

LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。

在遍历的时候会比HashMap慢TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器。当用Iterator遍历TreeMap时,得到的记录是排过序的。

 

参考链接

Class LinkedHashMap
ConcurrentModificationException with LinkedHashMap