Java LinkedHashMap 原理

Java LinkedHashMap 原理

LinkedHashMap是基于哈希算法,以键值对的形式存储和操作数据的非线程安全容器,继承于HashMap,在HashMap的基础上增加了双链表来支持插入顺序遍历,除此之外对操作顺序遍历也提供了支持,可用于实现LRU缓存
与HashMap一样在无哈希冲突情况下时间复杂度也能达到O(1),但由于双链表的维护,性能会稍低一些

LinkedHashMap在实现上很多方法直接继承HashMap,仅为维护双链表重写了一些方法,因此本文不介绍与HashMap相同的内容,有需要的可以看HashMap原理

内部结构

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;

LinkedHashMap相比HashMap增加了双链表的实现,使它能够支持键值插入顺序排序

Java LinkedHashMap 原理_第1张图片

final boolean accessOrder;

accessOrder是操作顺序排列的标志位,当在构造函数中指定该标志位时,双链表就会将当前操作的节点移出后插入链表尾部,来实现操作顺序排列

void afterNodeRemoval(Node<K,V> e) { // unlink
    ...
}
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    ...
}
void afterNodeAccess(Node<K,V> e) { // move node to last
    ...
}

HashMap中定义了三个空方法,而在LinkedHashMap中重写了这三个方法,这三个方法是专门留给LinkedHashMap用于维护双链表

初始化

构造函数不管有参无参都会调用父类HashMap的构造,不指定accessOrder时默认为false,表示按照插入顺序排序,否则就是操作顺序排序

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

remove

LinkedHashMap没有重写remove方法,而是直接使用父类的实现,父类最后会回调子类重写的afterNodeRemoval方法

final Node<K,V> removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        ...
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            ...
            // 回调子类方法
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

afterNodeRemoval方法用于实现双链表中的删除节点操作,如果待删除的节点是首节点或尾节点都会做相应处理

void afterNodeRemoval(Node<K,V> e) { // unlink
    // 将当前节点转为LinkedHashMap的节点,并获得该节点的前节点和后节点
    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;
}

get

LinkedHashMap中重写了get方法,但也调用了父类的getNode方法,最后如果按操作顺序排列则调用afterNodeAccess方法

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

afterNodeAccess方法用于将节点移动到双链表的尾部,节点若是首尾节点都会做相应处理

void afterNodeAccess(Node<K,V> e) { // move node to last
    // 定义最近操作节点
    LinkedHashMap.Entry<K,V> last;
    // 如果不是操作顺序排列或节点已经是尾节点则无需再做处理
    // 将尾节点设为最近操作节点
    if (accessOrder && (last = tail) != e) {
        // 将当前节点转为LinkedHashMap的节点,并获得该节点的前节点和后节点
        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

put方法LinkedHashMap同样没有重写,也是直接使用父类的put方法,但是调用newNode时会回调子类的newNode实现,当出现哈希冲突替换key相同节点时会回调子类的afterNodeAccess方法将该节点移动到双链表尾部,以及最后会回调子类的afterNodeInsertion方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
        ...
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 回调子类newNode方法
            tab[i] = newNode(hash, key, value, null);
        else {
            ...
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 回调子类afterNodeAccess方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
    ...
    // 回调子类afterNodeInsertion方法
    afterNodeInsertion(evict);
    return null;
}

LinkedHashMap重写的newNode方法最终会调用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;
}
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;
    }
}

afterNodeAccess方法用于移除最近最少被操作过的节点,自定义LRU缓存就可以通过将LinkHashMap设为按操作顺序排序并重写removeEldestEntry方法来实现

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // 判断是否移除最近最少被操作的节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        // 调用父类removeNode方法
        removeNode(hash(key), key, null, false, true);
    }
}

参考

https://segmentfault.com/a/1190000012964859
https://segmentfault.com/a/1190000021434024

你可能感兴趣的:(Java,面试相关)