美文网首页java源码学习
Java源码学习--LinkedHashMap

Java源码学习--LinkedHashMap

作者: 慕北人 | 来源:发表于2019-08-17 15:17 被阅读0次

Java源码学习--LinkedHashMap

JDK版本:1.8

LinkedHashMap内部维护了一个双向链表来保存元素。

一、重要属性

LinkedHashMap继承了HashMap,父类中的几个属性都原封不动的照用,除此之外,LinkedHashMap也有几个自己的属性:

  • head:是一个LinkedHashMap.Entry<K,V>类型对象,之后会说这个LinkedHashMap.Entry继承自HashMap.Node,是LinkedHashMap针对自己所定制的;这里代表内部所维护的双向链表的头部
  • tail:同head属性,这里代表双向链表的尾部
  • accessOrder:该属性是一个final修饰的boolean,主要作用目前已知是决定是否在访问了(put和get方法)某一个存在的key-value之后,将他们置于双向链表的尾部

二、Entry类

该类继承了HashMap.Node,是LinkedHashMap为自己量身打造的单位数据存储类型:

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

可以看到,只是添加了两个属性:after和before来实现双向链表;从这里也可以看出LinkedHashMap实现的一些端倪:LinkedHashMap中有两个链表(确切的说是两种),一个是由Entry中before和after属性共同实现的双链表;另一个是继承自HashMap中的Node的next属性构成的单链表。二者没有丝毫交集,完全独立工作。

三、重要方法

老规矩,先从构造器看起:

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}  

public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}  

public LinkedHashMap() {
    super();
    accessOrder = false;
}  

可见,默认都是将accessOrder设置为了false

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

关于构造器,就不多说了。

想要实现双链表,有一些必要的辅助方法不得不先说

1. linkNodeLast

该方法的作用是将所put的节点,添加到双链表的尾部:

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是否存在,之后才会决定是添加一个新的key-value对,还是覆盖原来key所对应的value。那么这个linkNodeLast显然只有在前者所对应的情况下才应该被调用,因此其被调用的位置就不得不斟酌斟酌:

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

而LinkedHashMap选择在newNode方法中来调用这个方法,在HashMap的putVal方法中确实在需要添加新的key-value对的时候会调用newNode方法新建一个节点这里是LinkedHashMap覆盖了父类的newNode方法

2. transferLinks

该方法的作用是将dst节点插入到src节点所在的位置:

private void transferLinks(LinkedHashMap.Entry<K,V> src,
                           LinkedHashMap.Entry<K,V> dst) {
    LinkedHashMap.Entry<K,V> b = dst.before = src.before;
    LinkedHashMap.Entry<K,V> a = dst.after = src.after;
    if (b == null)
        head = dst;
    else
        b.after = dst;
    if (a == null)
        tail = dst;
    else
        a.before = dst;
}  

注意,虽然将dst插入到了src所在的位置,但是src.before和src.after还是单方面的指向的原来的元素,这点并没有改变

3. afterNodeRemoval

该方法同afterNodeInsertion、afterNodeAccess都在HashMap中被定义,只是HashMap并没有去实现它们。

而这里的afterNodeRemoval在HashMap中调用的时机是在remove方法删除某个节点后调用的:

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

实际上猜也能猜到,这里肯定是将其从双向链表中删除的。

4. afterNodeAccess

该方法的调用时机是在HashMap调用putVal方法,且所对应的key已经存在的情况,从而覆盖原value后:

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    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;
    }
}

这个方法的作用是将被修改了的节点置为双向链表的表尾

5. afterNodeInsertion

该方法的调用时机是在HashMap调用putVal方法,且最终在table中添加了一个新的节点:

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

奇怪的是该方法的if语句的最后一个条件在LinkedHashMap中的实现为:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}  

所以,afterNodeInsertion相当于啥也没干

6. get

HashMap的常用方法中,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;
}  

可见和HashMap相比,只是多了第二个if语句而已。

LinkedHashMap每次get方法都会调用afterNodeAccess方法来将访问的节点置于双向链表的表尾,如果不想要这个效果,请将accessOrder设置为false

7. containsValue

该方法请勿和HashMap的containsKey混淆,HashMap只提供了containsKey方法,而containsValue方法是LinkedHashMap独创的:

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

该方法就是遍历双向链表来查找是否有目标value,其显然比containsKey要慢一些(因为containsKey首先会通过hash缩小范围)

四、非重要方法

LinkedHashMap只是重写了HashMap的entrySet、keySet、values三个,而他们各自对应的类,基本只是将其forEach方法从原来的for循环嵌套遍历改为了一个for循环遍历双向链表而已。

五、总结

重要知识点:

  1. LinkedHashMap中有两个链表:双向链表通过Entry的after和before属性构成;单项链表则继承自HashMap中Node的next属性。二者相互独立,没有交集
  2. LinkedHashMap中每次访问(put和get方法)已存在的key-value对之后,提供将他们置于双向链表表尾的服务,开启这个服务需要调用三个参数的构造器,将accessOrder置为true
  3. LinkedHashMap提供了containsValue方法共我们使用

一些收获:

最大的收获是LinkedHashMap对HashMap的继承,及HashMap优雅的结构。在HashMap中,预留了三个方法afterNodeInsertion、afterNodeAccess、afterNodeRemoval,而且在其对应的位置调用;因此LinkedHashMap只需实现这三个预留的方法即可,而不用覆盖HashMap的put方法。

要是我来写的话,可能不会再HashMap中预留出一些方法供子类调用,只会在子类中去单独定义,而且还会完全重写父类的put方法。显然使得子类变得臃肿,不够优雅。

相关文章

网友评论

    本文标题:Java源码学习--LinkedHashMap

    本文链接:https://www.haomeiwen.com/subject/dcqksctx.html