Map 源码阅读

Map学习

Map是java中存储key-value数据的容器类,按照惯例我们将从最底层的Map接口开始学习,了解其该拥有哪些基本的方法。

一 顶层接口

1. Map接口

先从接口注释学起,

  • Map是一个存储key-value映射关系的容器,key不可重复,keyvalue是一一对应的关系。

  • 可返回key集合、value集合、及映射关系集合,返回顺序取决于迭代器,或者根据子类特殊指定的顺序。

  • Map对象本身不能当做key,但可以作为value

public interface Map<K,V> {
    // 最大返回值也只能是Integer.MAX_VALUE
	int size();
    boolean isEmpty();
    // key 为null会报异常,key类型符合要也会抛ClassCastException
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    // 增删改查的逻辑
    V get(Object key);
    V put(K key, V value);
	V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    // 三个视图方法
    Set<K> keySet();
    Collection<V> values();
	Set<Map.Entry<K, V>> entrySet();
    
    boolean equals(Object o);
    int hashCode();
}

以上是Map中拥有的基本方法,从中可以看出最初的设计思路,

  • 作为一个容器类,需要考虑该容器存储什么类型的数组,这决定了增删改查方法的设计。
  • 还需要对提供容器的判空、是否包含指定元素、容器之间的比较等方法。
  • 最后容器类不仅关心容器内元素的进出,更需要考虑容器作为一个整体如何对外提供容器内数据展示,这就是三个视图方法的目的。

注意到上面entrySet方法返回的是一个Entry对象,这是一个键值对。

interface Entry<K,V> {
	K getKey();   
    V getValue();
    V setValue(V value);
    boolean equals(Object o);
    int hashCode();
}    

当然java8 在Map接口中新增了很多有默认实现的方法,这些方法在不修改子类的情况下扩展了Map的能力,下面将挑部分进行讲解。

getOrDefault

这是可以设置默认值的查询方法,当未找到key对于的value时返回默认值,而不是null

	default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }

forEach

内部遍历map,并对每个元素执行action的处理,

	default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

以上默认方法的实现都依赖于,接口中基础抽象方法,相当于把在外部经常使用map的一些模式提出来加入到Map中,方便了使用。

以前是砖块决定了楼层的搭建,现在是楼层搭建方式改变了砖块。

2.AbstractMap抽象类

AbstractMap中提供了Map接口中大部分的默认实现,但都依赖entrySet()方法获取kv集合。

二 具体实现

1.HashMap

先从注释看起

  • 非线程安全,允许key 或 value为null,不保证返回顺序。

  • loadFactor 是加载因子,默认为0.75 该变量意义就是
    实 际 数 组 大 小 > 数 组 容 量 ∗ l o a d F a c t o r 实际数组大小>数组容量*loadFactor >loadFactor
    就会分配进行扩容。

  • 如果开始就有很多数据需要存储,则最好初始化HashMap时就指定一个较大的容量。
  • 可以通过Collections中的synchronizedMap方法得到一个线程安全的Map。
  • 和collection集合类一样,在迭代时发生结构性变化会终止迭代,抛出ConcurrentModificationException

HashMap主要做的工作提供合理的计算哈希值的函数,且尽可能减少哈希冲突概率。这样就能使元素均匀散列在不同的bucket上,最大化体现复合型数据结构的优势。

存储结构

通过查看类中的成员变量我们可以快速了解HashMap的存储结构。

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
    // 默认初始化容量为16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 哈希表最大尺寸
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认装填因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当一个桶上链表长度大于8时会转换成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当一个桶上红黑树小于等于6时会转换为链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 当数组容量大于等于64时,才会将桶中的符合条件的链表转换为红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 实际存放数据的数组
    transient Node<K,V>[] table;
    // 键值对的副本
    transient Set<Map.Entry<K,V>> entrySet;
    // size实际大小
    transient int size;
    // 修改次数  版本号
    transient int modCount;
    // 数组扩容的阀值
    int threshold;
    // 装填因子
    final float loadFactor;
	
}

打脸了,看了上面的代码也不清楚HashMap是怎么存储数据的,数据结构中的哈希表使用哈希算法计算得出索引,基本方式是对数组容量取余,那就看下hash算法源码。

hash算法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 得出
index = (n-1) & (h = key.hashCode()) ^ (h >>> 16)

hash算法就是根据hash值生成一个数组索引。

index = node.hashCode % n

这种方式并不高效,采用实际采用的如下方式:

index = (n - 1) & hash(key);

首先我们的hash数组的length一定是2的幂,假设hashCode是51(0011 0011) 数组长度为16(0001 0000);由于length是2的幂则其二进制表示中肯定只有一个1,如:0000 1000 ,hashCode中高于这个1的位置的数据都能整除length,低于这个1的部分就是余数。

0011 0011 = 51
0001 0000 = 16
————————————————
0000 0011 = 3

通过位运算可以保留余数部分

0011 0011 & 0000 1111 = 0000 0011
而
0000 1111 = 0001 0000 - 0000 0001

所以 index = hashCode & (n-1) n为2的幂,得到了求余的方式。

上面根据取余的方式计算索引有个问题,当只有大于length的高位在变化时,求得的余数都是一样的。

即高位的变化影响不到余数,导致的结果就是冲突概率很高。

求解hash的部分,解决办法就是右移16位再与自身进行与运算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

求解hash的部分,解决办法就是右移16位再与自身进行与运算,hashCode 是32位的,右移16位将

高位数据和低位数据拉到了一起,进行与运算产生最终的hashCode。

举例
hashCode1 = 0101 0011 = 83
hashCode2 = 0001 0011 = 19
n = 0001 0000 = 16
index1 = hashCode1 & n-1 = 0101 0011 & 0000 1111 = 0011 = 3
index2 = hashCode2 & n-1 = 0001 0011 & 0000 1111 = 0011 = 3
# 此处我们实例使用8位,所以用4,。
hashCode1 = hashCode1 ^ (hashCode1 >>> 4) = 0101 0011 ^ 0000 0101 = 0101 0110
hashCode2 = hashCode2 ^ (hashCode2 >>> 4) = 0001 0011 ^ 0000 0001 = 0001 0010

index1 = hashCode1 & n-1 = 0101 0110 & 0000 1111 = 0000 0110 = 6
index2 = hashCode2 & n-1 = 0001 0010 & 0000 1111 = 0000 0010 = 2

经过以上算法处理,产生的hash索引分到了不同的位置。

小结:

(1)原始hash值与无符号右移一半位置的hash值做异或操作,得到结合高低位的新hash值。

(2)新hash值与n-1 做与操作得到最终索引。

插入节点

在插入流程基本可以看到HashMap的存储结构。

putVal

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    // tab是节点数组,i为hash对应下标,p为下标为i的位置对应节点,
    // i下标可能存在多个节点,p会依次向下变动
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //1.判断节点数组长度,如果为空或者length为0,则进行初始化。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //2.判断hash值对应坐标是否为空,如果为空则创建新节点并赋值。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    //3.不为空则说明坐标冲突,则先判断hash值在进行key比较。
        // e是与传入key相同的节点(暂定),k是相同节点对应key
        Node<K,V> e; K k;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
    	//4.判断节点是不是树节点,是则走树节点插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        //5.循环遍历链表节点,尾部为null时插入新节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 6.如果链表节点长度大于等于8则将其链表转换为树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //7. 如果找到匹配节点则中断遍历。
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //8. 没有找到匹配节点,p则向下移动
                p = e;
            }
        }
        //9. 找到匹配节点e,则返回旧节点值。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 查询到节点后的回调方法。
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //10. 前面两个分支,当p节点不为空则前面就返回了。
    // 当p节点为null时,插入了新节点(插入了数组中),对原有存储结构进行了变动(尺寸变动)。
    ++modCount;
    //11. 当数组尺寸大于扩容阀值,则触发数组扩容。
    if (++size > threshold)
        resize();
    // 在数组中插入元素后的回调方法
    afterNodeInsertion(evict);
    return null;
}

小结一下:

  1. 先判hashmap是否初始化过,否则进行首次扩容初始化。
  2. 判断Key的hash值计算得出的数组下标是否为null,如果为null则创建新节点插入数组。
  3. 如果不为空,则说明和已有节点索引冲突,需要将该节点插入到冲突节点字链表中或子树中。
  4. 如果是树结构,走单独插入逻辑。
  5. 如果是链表,则向下匹配,未找到则插入到链表末尾;判断链表长度,大于等于8则将其转换为树。
  6. 找到则暂存游标终止遍历,根据onlyIfAbsent决定是否覆盖,最后返回找到节点的旧值。
  7. 如果在数组中插入了新节点,需要判断是否进行扩容。

插入元素涉及三种数据结构,节点数组、节点链表、节点红黑树。插入元素先在数组中插入,下标有冲突则考虑插入链表中,链表长度>=8则转化为红黑树。

学习的小册中找到一张图,可以清晰的展示存储结构。

在这里插入图片描述

putTreeVal

普通插入算法中,如果是槽内根节点是红黑树,则需要走红黑树插入逻辑,putTreeVal是TreeNode中的方法,代码如下:

/**
* map 是要插入节点的hashmap
* tab 是存储节点的槽数组
* h k v 是要插入节点的hash值、key 和 value
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
    Class<?> kc = null;
    // 标记是否已经便利过一次树
    boolean searched = false;
    // 首先知道根节点
    TreeNode<K,V> root = (parent != null) ? root() : this;
    // 自旋
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        // p是遍历节点,ph是p节点hash值,dir是方向
        if ((ph = p.hash) > h)
            dir = -1;// 小于0说明要插入的h应该在p的左侧
        else if (ph < h)
            dir = 1;// 大于0说明要插入的h应该在p的右侧
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;// 匹配说明找到替换的节点了p,外层方法进行值替换操作。满足hash值相等和key相等两个条件
        else if ((kc == null &&
                (kc = comparableClassFor(k)) == null) ||
                (dir = compareComparables(kc, k, pk)) == 0) {
            // 此时两种情况:(1)k没有实现comparable接口,(2)k实现了comparable接口并且和p节点pk相同。
            // serarched标记p节点是否被遍历过,没有遍历则进入递归遍历逻辑。
            if (!searched) {
                // q是子树中找到的节点,ch是p的子节点。
                TreeNode<K,V> q, ch;
                searched = true;
                // 红黑树是平衡二叉树,这边使用短路逻辑,先从左侧遍历,再从右侧遍历,先找到先终止。
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                   ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            // 没找到可替换节点,需要进行插入逻辑,这里计算从哪边开始插入。
            dir = tieBreakOrder(k, pk);
        }
		// 这里首先根据插入方向dir对p进行更新,如果为空则进行新节点插入
        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next; //获取更新前p节点的next
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);// 创建新的树节点
            if (dir <= 0)
                xp.left = x; 
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            //balanceInsertion 对红黑树进行着色或旋转,以达到更多的查找效率,着色或旋转的几种场景如下
            //着色:新节点总是为红色;
            //如果新节点的父亲是黑色,则不需要重新着色;
            //如果父亲是红色,那么必须通过重新着色或者旋转的方法,再次达到红黑树的5个约束条件
            //旋转: 父亲是红色,叔叔是黑色时,进行旋转
            //如果当前节点是父亲的右节点,则进行左旋
            //如果当前节点是父亲的左节点,则进行右旋
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡
            return null; // 返回null 说明插入了操作产生了一个新的节点。
        }
    }
}
扩容算法

上面流程中涉及到了数组的扩容,这里也学习一下。扩容需要先了解下需要扩多大,

  1. 初次扩容使用tableSizeFor方法就是用于计算容量,得出的值总是2的幂。

tableSizeFor

计算容量,返回的是结果肯定是2的幂。其中右移后进行或操作。这个方法可以计算出大于等于cap 的最近的那个2的幂。

	static final int tableSizeFor(int cap) {
        int n = cap - 1; // 假设n = 9 = 00001001
        n |= n >>> 1;// n = 00001001 | 00000100 = 00001101 = 13
        n |= n >>> 2;// n = 00001101 | 00000011 = 00001111 = 15
        n |= n >>> 4;// n = 00001111 | 00000000 = 00001111 = 15
        n |= n >>> 8;// ... 
        n |= n >>> 16;// ...
        // 最后在对 n+1 得到 16
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  1. 使用resize()方法进行扩容,
final Node<K,V>[] resize() {
    // 复制旧有的Node数组
    Node<K,V>[] oldTab = table;
    // 旧容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧阀值
    int oldThr = threshold;
    // 新容量及阀值
    int newCap, newThr = 0;
    // 非初次赋值
    if (oldCap > 0) {
        // 如果旧容量已经超过了最大值则,就只修改阀值到最大。
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 如果新容量和阀值都是旧容量和阀值的两倍,<<1 
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // oldCap = 0 但 oldThr > 0 ,这里应该是减小容量的逻辑。
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 如果oldCap=0则说明未初始化。
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 使用装填因子*默认容量得到,初次扩容阀值。
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // 这里对应了oldCap = 0 但 oldThr > 0的情况,上面的逻辑没有对newThr赋值。
        float ft = (float)newCap * loadFactor;
        // 设置新的扩容阀值。
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    // 修改总体阀值
    threshold = newThr;
    // 创建新的容量的数组,并修改总体table。
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果原有节点数组不为空,则需要复制旧数据到新数组。
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 遍历获取元素并判空,源码中有很多相似逻辑
            if ((e = oldTab[j]) != null) {
                // 清空数组对应位置。
                oldTab[j] = null;
                if (e.next == null)
                    // 如果j位置只有一个节点(不是链表或树)
                    // 将旧节点填充到新数组相应位置
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 如果节点是树节点,将树节点拆分填充到新数组中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 如果节点是链表
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        // 取到节点的下一个元素
                        next = e.next;
                        // oldCap是2的幂,所以只有一位是1其他位都是0
                        // 这个算法将一个长链表拆分再连为两部分。
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                // 头指针只赋值一次
                                loHead = e;
                            else
                                // 尾指针赋值
                                loTail.next = e;
                            // 尾指针移动
                            loTail = e;
                        }
                        else {
                            // 
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 重新对桶赋值,使用刚拆分出来的低位链表。
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 对扩充后j + oldCap 的桶赋值,使用刚拆封出来的高位链表。
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

2.TreeMap

TreeMap是基于红黑树实现的,确保了其containsKey、get、put、remove 等操作的复杂度都是log(n)

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    // 可通过构造函数传入比较器,决定了map中key的顺序,否则使用元素原生的比较器。
    private final Comparator<? super K> comparator;
    // 红黑树根节点
    private transient Entry<K,V> root;
    // map内元素个数
    private transient int size = 0;
    // 结构性修改次数
    private transient int modCount = 0;
    // TreeMap的视图结构
    private transient EntrySet entrySet;
    private transient KeySet<K> navigableKeySet;
    private transient NavigableMap<K,V> descendingMap;
}

小结:

  1. TreeMap中key的顺序取决于比较器,优先使用外部传入比较器,其次使用key自带的比较方法。
  2. TreeMap实现了NavigableMap接口,拥有一系列导航定位的方法。
  3. TreeMap实现了Cloneable接口和Serializable接口,可以被序列化和克隆。
存储结构

既然是红黑树,这里看下节点结构。

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;// 当前节点key
        V value;// 当前节点value
        Entry<K,V> left; 
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;// 节点默认颜色为黑色
}

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

  • 性质1:每个节点要么是黑色,要么是红色。
  • 性质2:根节点是黑色。
  • 性质3:每个叶子节点(NIL)是黑色。
  • 性质4:每个红色结点的两个子结点一定都是黑色。
  • 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
插入节点

put方法会将新节点插入树中,如有相同key则进行替换操作并返回旧value,如果是新增节点则返回null。

public V put(K key, V value) {
    // 1.根节点如果为空则创建新节点
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // 确保key不能为null
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    // 2.如果TreeMap自带比较器则使用自带比较器进行比较。
    if (cpr != null) {
        do { // 自旋遍历红黑树,依次进行比较,parnent 记录了未匹配到key,终止循环时的父节点
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
				// key 小于t.key则说明匹配位置应该在左子树上
                t = t.left;
            else if (cmp > 0)
                // key 大于t.key则说明匹配位置应该在右子树上
                t = t.right;
            else
                // key相同则进行替换赋值
                return t.setValue(value);
        } while (t != null);
    }
    else {
        // 使用元素自带比较器进行遍历
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    // 2.此时说明树中没有key相同节点,创建新节点。
    Entry<K,V> e = new Entry<>(key, value, parent);
    // 在最后的父节点中插入新节点
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    // 插入节点后进行旋转着色操作,维护平衡性。
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

小结:

  1. 首先是查找过程,利用红黑树特性快速查找与key匹配的节点,找到匹配节点则替换新值返回旧值,未找到则记录了可供插入的叶子节点的父节点(子节点为null的节点)。
  2. 插入过程,创建新节点插入到父节点中,着色旋转维护平衡性。
  3. TreeMap不允许使用null为key。
插入自平衡
private void fixAfterInsertion(Entry<K,V> x) {
    // 首先将新插入节点置为红色
    x.color = RED;
	
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}
删除节点

删除节点使用的remove方法,内部实现是deleteEntry方法,这里可以了解一下:

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;
    // 情况一:p有两个子节点
    
	// 当左右节点都不为null时,使用successor找到p节点对应的前驱或者后继节点。
    // 当把红黑树上的节点投射到与叶子层平行的水平线上时,节点序列是按从左到右的顺序依次递增的。
    // 前驱节点就是p的前一个相邻节点,后继节点就是p的后一个相邻节点。
    // 删除p节点相当于把s节点移动到p的位置上,再在原有位置上删除s节点。
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    }

    // 如果最开始p拥有两个子节点,经过上面过程,p已经被替换为前驱或后继节点s。
    // 而前驱节点肯定没有right节点,后置节点肯定没有left节点。
    // 所以:此时的p 只有一个子节点,另一个子节点为null
    
    // 情况二:p只有一个子节点 ,replacement就是要替换p的子节点。
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
	
    if (replacement != null) {
        // 将子节点replacement连接到parent节点上。
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // 如果p.color是RED,其子节点一定是黑色的,删除一个红色节点替换为黑色子节点,
        // 对该子节点而言通往root路径上的黑色节点数量不变,平衡不变。
        
        // 如果p.color是BLACK,删除一个黑色节点就需要调整平衡性。
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        // 情况三:p没有子节点
        // p为黑色叶子节点,删除p需要调整平衡性。
        if (p.color == BLACK)
            fixAfterDeletion(p);
		// p的left和right都是null了,此处要清空p和红黑树的关系。
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}
删除自平衡

以上插入和删除操作都会涉及到自平衡方法fixAfterDeletion,它对删除后补位的元素进行调整,这里看下该方法的实现过程。

private void fixAfterDeletion(Entry<K,V> x) {
    // x是被删除所在位置的节点,(有可能是补位的节点,也有可能是被删除的节点)
    // 红黑树的平衡是从底部到顶部(根部)的过程
    while (x != root && colorOf(x) == BLACK) {
        // 情况1:被替换的节点是左节点
        if (x == leftOf(parentOf(x))) {
            // x的兄弟节点
            Entry<K,V> sib = rightOf(parentOf(x));
			// 1.1 兄弟节点为红色
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);// 将兄弟节点置为黑色 
                setColor(parentOf(x), RED);// 将父节点置为红色
                rotateLeft(parentOf(x)); // 父节点的右侧黑色节点层数变多,以父节点为中心左旋调整。
                sib = rightOf(parentOf(x));// 左旋后原来父节点right节点会变动,更新兄弟节点sib。
            }
			// 1.2 替换节点的兄弟节点是黑色(原本是黑色或经上一步改为黑色),
            // 兄弟节点的子节点都是黑色
            if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);// 将兄弟节点改为红色
                x = parentOf(x);// 将x指向x节点的父节点
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    // 1.3 兄弟节点的右子节点是黑色,左子节点是红色
                    setColor(leftOf(sib), BLACK);// 将左节点置为黑色
                    setColor(sib, RED);// 兄弟节点由黑转红
                    rotateRight(sib);// 兄弟节点的left黑色节点层数变多,右旋调整
                    sib = rightOf(parentOf(x));// 更新兄弟节点
                }
                // 1.4 兄弟节点左子节点
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // symmetric
            // 被替换的节点是右节点
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}
查找元素

查找元素逻辑和插入元素的逻辑类似,代码如下:

final Entry<K,V> getEntry(Object key) {
    // 同样优先使用TreeMap中的比较器进行查找
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    // 从根节点进行遍历查找
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

3.LinkedHashMap

HashMap是无序的,TreeMap是按照Key的大小进行的排序,而LinkedHashMap是按照插入的顺序进行排序的,其最重要的两个特性是:

  • 可以按照插入顺序进行访问
  • 可以实现LRU

下面的学习主要关注以上两点的实现。

存储结构
public class LinkedHashMap<K,V> extends HashMap<K,V>implements Map<K,V>{
    // 双向链表头结点,维护的是最早插入的节点
	transient LinkedHashMap.Entry<K,V> head;
    // 双向链表尾节点,维护的是最新插入的节点
    transient LinkedHashMap.Entry<K,V> tail;
    // 访问顺序标记符,false为按插入顺序方法
    // true为按访问顺序,会调整节点顺序,将经常访问节点放到尾部
	final boolean accessOrder;
    // 扩展节点,增加了前驱节点before 和 后继节点after
   	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);
        }
    } 
}

LinkedHashMap继承HashMap,所以底层数据存储依旧是数组+链表+红黑树,不同的地方在于其扩展了节点,

按插入顺序维护了一个双向链表,这是实现按插入顺序进行访问的关键。

插入节点

上面可知LinkedHashMap中扩展的节点有两个新增属性,这两个属性是什么时候赋值的呢?LinkedHashMap自己没有实现插入方法,使用的是HashMap中的put方法,但覆写了newNode和newTreeNode方法,在里面完成了前驱和后继的连接过程。

调用过程如:put->putVal->newNode/newTreeNode->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;
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    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的前驱节点和链表尾节点连接
        p.before = last;
        last.after = p;
    }
}

通过以上过程,每个新增节点都会被按插入顺序插入到链表中。

删除节点

删除节点的方法同样使用HashMap中的remove方法,但还需要从双向链表中删除该节点才算完全删除。

这里就用到了在HashMap中删除元素后的回调方法afterNodeRemoval,linkedHashMap覆写了这个方法,其中实现了从双向链表中删除节点的逻辑。

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // p是待删除节点,b是前驱节点,a是后继节点
    p.before = p.after = null;
    if (b == null)
        head = a;// 前驱节点为null,则将head指向后继节点
    else
        b.after = a;// 前驱节点不为null,则将前驱节点和后继节点连起来
    if (a == null)
        tail = b;// 后继节点为null,则将tail指向前驱节点
    else
        a.before = b;// 后继节点不为null,则将后继节点和前驱节点连起来
}
顺序访问

通过LinkedHashIterator 可以依次访问LinkedHashMap中所有元素,访问过程如下:

LinkedHashIterator() {
    next = head;// 头结点作为访问的第一个节点
    expectedModCount = modCount;
    current = null;
}

public final boolean hasNext() {
    return next != null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
    LinkedHashMap.Entry<K,V> e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    current = e;
    next = e.after;
    return e;
}

因为在插入节点时,已经按照插入顺序将节点插入到双向链表当中,所以在遍历时只需要按顺序依次访问e.after就得到了节点插入顺序。

LRU实现

LRU是指最近最少使用,是一种页面置换算法,会将不经常使用的页面删除,经常用于设计缓存。Android中的LruCache其底层数据存储就是基于LinkedHashMap。

  • 将访问过的节点放到队尾(队尾是最新插入的节点)
  • 插入元素后如果满足删除策略,就删除最少访问的节点,新节点插入到队尾

下面使用一个demo来展示如何使用linkedHashMap来实现LRU

public static void testLru(){
    LinkedHashMap<Integer,Integer> lruMap = 
        // 调用构造函数,指定accessOrder为true
        new LinkedHashMap<Integer, Integer>(0,0.75f,true){
        	// 覆写删除策略,这里指定元素个数超过3则删除旧节点
        	@Override
        	protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            	return size()>3;
        	}
    };

    lruMap.put(1,1);
    lruMap.put(2,2);
    lruMap.put(3,3);
    lruMap.put(4,4);
    lruMap.put(5,5);
    System.out.println(lruMap);
	// {3=3, 4=4, 5=5}  插入了5个元素,根据删除策略,最早的两个节点被删除了

    lruMap.get(5);
    lruMap.get(4);
    lruMap.get(3);
    System.out.println(lruMap);
    // {5=5, 4=4, 3=3} 最近访问的节点会被移动到队尾
}

问题1:插入节点时,何时删除了旧节点?

linkedHashMap 没有实现put方法,当调用父类HashMap的put方法时会调用afterNodeInsertion方法,通过覆写该方法实现删除逻辑。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // 在允许删除并且删除策略为true时,删除头部节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

问题2:查询节点时,何时将访问节点移动到了队尾?

在调用get或getOrDefault 方法时都会回调afterNodeAccess方法,LinkedHashMap覆写了该方法,并在其中完成

public V get(Object key) {
    Node<K,V> e;
    // 使用HashMap中getNode方法获取节点
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)// 如果设置了按访问顺序读取
        afterNodeAccess(e);// 将访问的节点放置到队列尾部
    return e.value;
}

public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return defaultValue;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        // p是要移动的节点,b是p前置节点,a是p后置节点
        LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e
            , b = p.before, a = p.after;
        p.after = null;// 先断开p的后置连接
        if (b == null)
            head = a;// 如果前置节点b,不存在则将后置节点a设为头节点
        else
            b.after = a;// 否则将前置节点b和后置节点a连起来
        if (a != null) // 后置节点a不为null则将后置节点a和前置节点b连起来
            a.before = b;
        else
            last = b;// 后置节点a为null则将前置节点b设为尾节点
        if (last == null)
            head = p;// 尾节点为null则将p置为头节点
        else {
            p.before = last;// 尾节点不为null则将p节点插入到尾节点,
            last.after = p;
        }
        tail = p;// 尾节点指向p
        ++modCount;
    }
}

上面调整的过程可分为两部分,先把p节点摘下来,再将p节点插入到链表尾部。

三 总结

上面介绍了Java集合类中Map相关的代码,首先从Map接口讲起,Map集合用于存储Key-Value格式的数据,作为一个数据集合,提供了增删改查的接口规范以及描述集合整体数据的视图方法。具体实现部分先讲了HashMap,其底层数据存储是数组+链表+红黑树 ,围绕该存储结构讲解了hash算法和扩容算法。TreeMap底层是红黑树,节点按照key的大小进行了排序,这里的重点是插入删除节点后红黑树的自平衡。最后是LinkedHashMap是对Hashmap 的扩展,通过将节点维护成一个双向链表,保留了节点的插入顺序,通过覆写几个回调方法实现了LRU算法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值