LinkedHashMap源码详解

来源:互联网 发布:安卓微信数据迁移 编辑:程序博客网 时间:2024/06/03 18:54

LinkedHashMap源码详解

LinkedHashMap是基于HashMap实现的,如果对HashMap不了解,请先学习HashMap:http://blog.csdn.net/luanmousheng/article/details/75195809

HashMap的无序性和LinkedHashMap的有序性

前面介绍的HashMap查找效率很高,但是也有一个缺点,即遍历HashMap是无序的。LinkedHashMap顾名思义,是链表和哈希表的结合,链表具有天然的有序性,这里的有序不是按照节点大小排序,而是按照节点的插入顺序排序或者节点的访问顺序排序。

为了比较HashMap和LinkedHashMap的有序性,首先观察HashMap的遍历结果:

  Map<String, String> map2 = new HashMap();  map2.put("name", "jack");  map2.put("age", "23");  map2.put("job", "student");  map2.put("home", "china");  Iterator<Map.Entry<String, String>> it2 = map2.entrySet().iterator();  while(it2.hasNext()) {    System.out.println(it2.next().getKey());  }

这段代码段输出:

homeagenamejob

可以看到,遍历HashMap时的输出和输入时的顺序没有关系。

再观察LinkedHashMap的遍历结果:

  LinkedHashMap<String, String> map = new LinkedHashMap();  map.put("name", "jack");  map.put("home", "china");  map.put("age", "23");  map.put("job", "student");  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();  while(it.hasNext()) {    System.out.println(it.next().getKey());  }

这段代码输出:

namehomeagejob

可以看到,默认情况下,遍历LinkedHashMap时的输出和输入的顺序是一致的。因此我们说LinkedHashMap是可以保证有序性的。

我们说默认情况下,遍历LinkedHashMap时的输出和输入的顺序是一致的,LinkedHashMap还可以根据节点的访问顺序进行排序,即最新访问的节点放在最前面。LinkedHashMap提供了accessOrder字段,这个字段可以指示LinkedHashMap是否按照访问时间进行排序,通过LinkedHashMap另一个带参数的构造函数可以创建一个按照访问时间排序的哈希表,看下面的例子:

  LinkedHashMap<String, String> map = new LinkedHashMap(10, 0.75F, true);  map.put("name", "jack");  map.put("home", "china");  map.put("age", "23");  map.put("job", "student");  //访问了键"home"的节点  map.get("home");  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();  while(it.hasNext()) {    System.out.println(it.next().getKey());  }

将4个键值对添加到LinkedHashMap后,访问了键”home”的节点,这段代码的输出为:

nameagejobhome

可以看到,键”home”被我们访问后,放到了最后一个位置(最新的位置),这就是LinkedHashMap按照访问先后顺序的有序性。

好了,对于LinkedHashMap有了基本的认识后,下面将基于源码,详细介绍LinkedHashMap的原理。

LinkedHashMap原理

LinkedHashMap的存储还是通过HashMap实现的,但是它和HashMap最大的区别在于LinkedHashMap维护了一个双向链表,这个双向链表按照节点的插入顺序保存节点、或者按照节点的访问先后顺序保存节点。

先看下LinkedHashMap的声明:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {  //……  private static class Entry<K,V> extends HashMap.Entry<K,V> {        Entry<K,V> before, after;        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {            super(hash, key, value, next);        }        //……  }}

LinkedHashMap继承了HashMap,内部静态类Entry继承了HashMap的Entry,但是它和HashMap.Entry不同的是,LinkedHashMap.Entry多了两个字段:beforeafter,before表示在本节点之前添加到LinkedHashMap的那个节点,after表示在本节点之后添加到LinkedHashMap的那个节点,这里的之前和之后指时间上的先后顺序

有了对LinkedHashMap.Entry的了解,通过示意图学习LinkedHashMap的工作原理。

这里写图片描述

图1 LinkedHashMap初始状态

LinkedHashMap的初始状态包括一个HashMap和一个只有头节点的双向链表。

接着插入键K1:

这里写图片描述

图2 插入键K1的状态

接着插入键K2:

这里写图片描述

图3 插入键K2的状态

通过以上三个示意图,基本上可以理解LinkedHashMap的工作原理,示意图的左边部分是HashMap,右边部分是双向链表,这个双向链表记录了键的添加顺序。注意,这里我们将HashMap中的键和链表中的键分开表示,其实它们是同一个节点,分开后利于观察,否则很多指针纠缠在一起,示意图会显得很混乱。

以上三个示意图都是基于插入顺序排序的,假设当前LinkedHashMap的状态如图3,并且我们创建该哈希表时候指定了按访问时间排序,当我们在图3的基础上分别添加K3、访问K2后的状态为:

这里写图片描述

图4 分别添加K3、访问K2后的状态

在图3的基础上,添加K3、访问K2后将K2移到了链表的末尾。

将LinkedHashMap的accessOrder字段设置为true后,每次访问哈希表中的节点都将该节点移到链表的末尾,表示该节点是最新访问的节点。

好了,我们通过几个示意图已经了解了LinkedHashMap的工作原理,接着学习LinkedHashMap的源码。

LinkedHashMap源码解析

我们使用哈希表是为了往哈希表中添加键值对,那我们就从最基本的方法put说起。

put方法

这里写图片描述

图5 LinkedHashMap中搜索put方法

在idea中搜索LinkedHashMap的put方法,惊奇的发现,LinkedHashMap中并没有定义put方法,相反,idea向我们推荐了很多put的实现,显然我们应该去看HashMap的put实现,也就是说,LinkedHashMap的方法即是HashMap的put方法,当我们调用LinkedHashMap的put方法实际上调用的是父类HashMap的put方法。

看下HashMap的put实现:

public V put(K key, V value) {    if (table == EMPTY_TABLE) {      inflateTable(threshold);    }    if (key == null)      return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      Object k;      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {        V oldValue = e.value;        e.value = value;        //1:HashMap中是个空实现,子类LinkedHashMap的Entry实现了该方法        e.recordAccess(this);        return oldValue;      }    }    modCount++;    //2:子类LinkedHashMap重写了该方法    addEntry(hash, key, value, i);    return null;}

上面代码1处和2处正是LinkedHashMap的不同之处,LinkedHashMap重写了HashMap.Entry的recordAccess方法和HashMap的addEntry方法。其中recordAccess方法在父类HashMap的Entry是个空实现,子类LinkedHashMap.Entry重写该方法是为了记录节点访问的先后顺序。

是时候介绍LinkedHashMap的Entry类了:

private static class Entry<K,V> extends HashMap.Entry<K,V> {    //before节点在当前节点之前插入    //after节点在当前节点之后插入    Entry<K,V> before, after;    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {      //直接调用了父类的构造函数      super(hash, key, value, next);    }    //将当前节点从该双向链表中删除    private void remove() {      before.after = after;      after.before = before;    }    //将当前节点插入指定节点的前面    //其实就是改变链表的指针指向    private void addBefore(Entry<K,V> existingEntry) {      after  = existingEntry;      before = existingEntry.before;      before.after = this;      after.before = this;    }    //这是LinkedHashMap与HashMap重要的不同之处    void recordAccess(HashMap<K,V> m) {      LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;      if (lm.accessOrder) {        //若是节点访问先后顺序的规则        lm.modCount++;        //先把当前节点删除,然后把该节点添加到头节点的前面,也就是链表的末尾,参考示意图2和图3        remove();        addBefore(lm.header);      }    }    void recordRemoval(HashMap<K,V> m) {      remove();    }}

这段代码最重要之处在于对accessOrder的判断,若是基于访问顺序,每次访问节点后需要将该节点移到链表的末尾处,否则recordAccess什么也不做。其他的代码就是对双向链表指针指向的改变,参考图2、3、4。

accessOrder的意义在于,每次访问一个节点都将该节点移到链表的末尾,表示这个节点是最新访问的节点,这在很多场景下都很有用处,比如LRU算法,排在链表末尾部分的节点都是最近使用过的节点,那么排在前面部分的节点就可能长期都没有被访问过,此时系统可以将这些节点删除以增加可用内存。

好了,现在继续看put方法调用的addEntry方法。子类LinkedHashMap重写了HashMap的addEntry方法,看下LinkedHashMap的addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {    //调用了父类HashMap的addEntry方法    super.addEntry(hash, key, value, bucketIndex);    // 如果需要的话删除在链表中最久的节点    Entry<K,V> eldest = header.after;    //removeEldestEntry是个protected方法。    //LinkedHashMap中该方法返回false,也就是不会删除在链表中最久的节点。    if (removeEldestEntry(eldest)) {      removeEntryForKey(eldest.key);    }}

LinkedHashMap的addEntry方法首先调用了父类的addEntry方法,注意,子类可以重写removeEldestEntry方法并返回true,删除在链表中最久的节点。

到此为止,似乎并没有找到令我们多么兴奋的事,毕竟LinkedHashMap的removeEldestEntry返回了false,也就是LinkedHashMap只是调用了父类HashMap的addEntry方法,并没有做其他的事。继续深究HashMap的addEntry方法,能发现一些不同的地方:

void addEntry(int hash, K key, V value, int bucketIndex) {    if ((size >= threshold) && (null != table[bucketIndex])) {      resize(2 * table.length);      hash = (null != key) ? hash(key) : 0;      bucketIndex = indexFor(hash, table.length);    }    //LinkedHashMap重写了该方法    createEntry(hash, key, value, bucketIndex);}

LinkedHashMap重写了父类的createEntry方法,让我们继续看LinkedHashMap的createEntry方法:

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;    //1:将节点e加入到header的前面,也就是链表的末尾    e.addBefore(header);    size++;}

比较HashMap和LinkedHashMap,我们发现LinkedHashMap在1处有不同,其他地方都完全相同。

LinkedHashMap创建结点并将该节点添加到HashMap后,该节点会被链接到头结点header链表的末尾,在这里实现了LinkedHashMap插入的有序性。

构造函数

到这里解释完了put方法,接下来看下LinkedHashMap的构造方法,LinkedHashMap有很多重载的构造方法,原理大致大同,这里介绍其中一个构造方法来讲解:

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

这个带3个参数的构造函数其中initialCapacity和loadFactor决定了哈希表的初始容量和加载因子,accessOrder决定了LinkedHashMap的排序规则,如果accessOrder=false,则按插入顺序排序,否则按访问顺序排序。

该构造函数调用了父类的构造函数,看下HashMap中的构造函数:

public HashMap(int initialCapacity, float loadFactor) {    if (initialCapacity < 0)      throw new IllegalArgumentException("Illegal initial capacity: " +                                         initialCapacity);    if (initialCapacity > MAXIMUM_CAPACITY)      initialCapacity = MAXIMUM_CAPACITY;    if (loadFactor <= 0 || Float.isNaN(loadFactor))      throw new IllegalArgumentException("Illegal load factor: " +                                         loadFactor);    this.loadFactor = loadFactor;    threshold = initialCapacity;    //HashMap中该方法是个空方法,子类LinkedHashMap重写了该方法    init();}

父类HashMap中的init方法是个空实现,子类LinkedHashMap重写了该方法,看下LinkedHashMap中init方法的实现:

void init() {    header = new Entry<>(-1, null, null, null);    header.before = header.after = header;}

该方法创建了一个只带头节点的链表,参考图1。

LinkedHashMap的所有构造函数都会调用父类HashMap的构造函数,HashMap的构造函数都会调用init方法,即我们创建LinkedHashMap时总会调用init方法创建一个只带头节点的链表。

get方法

public V get(Object key) {    Entry<K,V> e = (Entry<K,V>)getEntry(key);    if (e == null)      return null;    //看这里,LinkedHashMap的不同之处    e.recordAccess(this);    return e.value;}

这里需要注意e.recordAccess的调用,这个调用之前我们已经分析过,每次访问一个节点的时候都要根据accessOrder是否为true,决定是否将该节点移到链表末尾,表示该节点是最近访问的节点,实现按照访问顺序的有序性。

containsValue方法

public boolean containsValue(Object value) {    if (value==null) {      //在链表中查找值为null的节点      for (Entry e = header.after; e != header; e = e.after)        if (e.value==null)          return true;    } else {      //在链表中查找值为value的节点      for (Entry e = header.after; e != header; e = e.after)        if (value.equals(e.value))          return true;    }    return false;}

LinkedHashMap的containsValue方法利用了该哈希表的链表特性,在链表中查找是否存在对应的值。

transfer方法

void transfer(HashMap.Entry[] newTable, boolean rehash) {    int newCapacity = newTable.length;    //利用LinkedHashMap的链表特性将节点放到新的哈希表    for (Entry<K,V> e = header.after; e != header; e = e.after) {      if (rehash)        e.hash = (e.key == null) ? 0 : hash(e.key);      int index = indexFor(e.hash, newCapacity);      e.next = newTable[index];      newTable[index] = e;    }}

该方法将原来哈希表中的Entry转移到新的哈希表,通过遍历链表将节点放到新的哈希表中。

迭代器

LinkedHashMap的迭代器是通过它的内部类实现的,其中最主要的类是LinkedHashIterator,该类是个抽象类,提供了基本的迭代方法。Entry迭代器、Key迭代器和Value迭代器都是通过继承该抽象类实现的。首先看下LinkedHashIterator:

private abstract class LinkedHashIterator<T> implements Iterator<T> {    //从链表头结点的后一个节点开始遍历    Entry<K,V> nextEntry    = header.after;    //保存了最近访问到的节点    Entry<K,V> lastReturned = null;    //该字段用于快速失败机制,当迭代器发现这两个值不相等,说明有其他线程改变了    //该哈希表,抛出ConcurrentModificationException    int expectedModCount = modCount;    //如果下个节点是头节点,说明遍历结束    public boolean hasNext() {      return nextEntry != header;    }    public void remove() {      //最近返回的节点为null,不能删除      if (lastReturned == null)        throw new IllegalStateException();      //快速失败机制,抛出并发修改异常      if (modCount != expectedModCount)        throw new ConcurrentModificationException();      LinkedHashMap.this.remove(lastReturned.key);      //每迭代一次只能删除一次,不能迭代一次删除多次      lastReturned = null;      expectedModCount = modCount;    }    Entry<K,V> nextEntry() {      //快速失败机制      if (modCount != expectedModCount)        throw new ConcurrentModificationException();      //不存在下个节点      if (nextEntry == header)        throw new NoSuchElementException();      //保存最近访问的节点      Entry<K,V> e = lastReturned = nextEntry;      nextEntry = e.after;      return e;    }}

LinkedHashIterator还是比较好理解的,和HashMap的迭代器类似,直接看源码注释好了。

EntryIterator、KeyIterator和ValueIterator都是通过继承LinkedHashIterator实现的:

private class KeyIterator extends LinkedHashIterator<K> {    //键的迭代器    public K next() { return nextEntry().getKey(); }}private class ValueIterator extends LinkedHashIterator<V> {    //值的迭代器    public V next() { return nextEntry().value; }}private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {    //Entry的迭代器    public Map.Entry<K,V> next() { return nextEntry(); }}

总结

好了,LinkedHashMap的源码就解释完了,做下总结:

  1. LinkedHashMap是双向链表和HashMap的完美结合。
  2. LinkedHashMap是有序的,默认通过插入顺序排序,也可以通过构造函数的参数accessOrder指定通过访问顺序排序。
  3. 只要理解了HashMap的工作原理,就很容易理解LinkedHashMap,它只是在HashMap的基础上将各个Entry节点通过双链表链接起来实现有序性。
原创粉丝点击