MyHashMap/README.md
2024-05-03 16:43:25 +08:00

10 KiB
Raw Permalink Blame History

HashMap笔记

Node类

Node类有hash、key、value、下一个节点地址等4个属性可以构成一个单项链表为HashMap中存储数据的基本类型

static class Node<K,V> implements Map.Entry<K,V> {
    // 经过计算之后的hash值
    final int hash;
    final K key;
    V value;
    // 链表中下一个节点的地址
    Node<K, V> next;

    Node(int hash, K key, V value, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

TreeNode类

TreeNode类可以构成一个红黑数为HashMap中链表树化之后的类型

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K, V> parent;  // red-black tree links
    TreeNode<K, V> left;
    TreeNode<K, V> right;
    TreeNode<K, V> prev;    // needed to unlink next upon deletion
    boolean red;

    TreeNode(int hash, K key, V val, Node<K, V> next) {
        super(hash, key, val, next);
    }
}

HashMap属性

hash表

HashMap中有个table属性用于存储键值对数据即是hash表。 其类型为Node数组数组中每个位置称为桶桶中保存链表或者红黑树的头节点地址组成了一个二维的数据结构 数组的长度即为HashMap的表容量

transient Node<K,V>[] table;

默认初始容量

默认为16即不指定初始容量的情况下hash表数组长度为16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

最大初始容量

static final int MAXIMUM_CAPACITY = 1 << 30;

默认负载因子

负载因子用于计算hash表扩容值当hash表桶数量大于 负载因子与表容量的乘积需要对hash表进行扩容

static final float DEFAULT_LOAD_FACTOR = 0.75f;

###链表树化阈值

当hash表某个桶中的链表元素大于树化阈值的时候且桶个树大于树化最小表容量的时候就要将这个链表转化为红黑树。 这个值必须大于2并且应该至少为8

static final int TREEIFY_THRESHOLD = 8;

非树化阈值

hash表某个桶中保存的是红黑树且树的节点个数小于非树化阈值的时候红黑树将转化为链表 这个值应小于链表树化阈值且最多为6

static final int UNTREEIFY_THRESHOLD = 6;

树化最小表容量

链表树化前需要判断是否达到了最小表容量即判断hash表的大小是否大于这个值 这个值应该大于等于4倍链表树化阈值以避免调整大小和树化阈值之间的冲突

static final int MIN_TREEIFY_CAPACITY = 64;

负载因子

HashMap中空值扩容阈值的属性

final float loadFactor;

扩容阈值

控制hash表扩容的阈值该值通过 负载因子 * hash表容量计算得到。 初始化的时候不指定表容量这个值为0初始化时指定表容量这个值等于计算后的表容量 但是第一次put的时候会将表容量与负载因子相乘计算并给这个树形重新赋值

int threshold;

hash表操作次数

put、remove、merge、clear等操作都会使得该值增加 在foreach、replaceAll等会影响hash表结构的操作期间会判断这个值是否变化 ,如果变化了,会快速失败

transient int modCount;

hash表索引计算&表容量为2的幂次方原因

元素在放入hash表中某个桶的时候需要通过hash算法计算出桶位置即hash表索引数组下标。 计算公式为 hash & (table.length - 1)

如果hash表容量为2的幂次方即table.length = 2^n那么该公式即为计算hash值除以表容量后的余数。 通过%同样可以获取到余数但是位运算更加高效所以这里需要限定hash表容量必须是2的幂次方。

通过表索引公式可以将任意hash值转变为0到表容量范围的整数可以通过该整数直接为元素分配存储的桶。

通过简单的代码对公式可以进行验算:

public class Test {
    public static void main(String[] args) {
        int length = 16;
        for (int i = 0; i < 50; i++) {
            int hash = i & (length - 1);
            System.out.println(i + " -> " + hash);
        }
    }
}

hash计算

HashMap中用于索引计算的hash值不是key的hash值而是通过hash计算方法得到的。 算法为hash值异或hash值无符号右移16位后的值。

hash无符号右移16位之后高16位会变为0异或计算不会改变原hash值的高16位但是会改变低16位 将高16位与低16的特征将结合使得hash值的低16位更加散列

当两个hash值高位不同但是低位相同的时候通过hash索引算法高位会被屏蔽得到的结果是相同的 容易造成hash冲突将低位特征与高位特征相结合可以减少hash冲突提高效率

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

扩容

在HashMap添加数据的时候会做扩容判断HashMap#putVal方法中:

    // sizehash表容量即table数组的长度
    // threshold扩容阈值一般为0.75倍表容量
    if (++size > threshold){
        resize();
    }

HashMap的扩容逻辑在 HashMap#resize 方法中。

// oldCap旧表容量
// newCap新表容量
// newThr 、oldThr新旧扩容阈值
 if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
                //扩容,新表容量*2 ,新阈值*2
                newThr = oldThr << 1;
            }
        } 

hash表容量限定都是2的幂次方扩容时直接按位左移一位即表容量乘以2. 但是在扩容之后表容量变化了表索引的计算结果也会改变。此时需要重新hash并移动元素保存位置。 对于同一个链表的节点来说,索引计算结果也会改变,所以链表的节点也会移动位置。

但是容量以及索引计算公式都与2的幂次方有关所以节点移动只有两种结果 1是位置不变2是移动到 当前索引 + 旧容量值 的桶上。

 if (oldTab != null) {
            //oldCap旧表容量遍历旧表数组移动元素到新表上
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null){
                        // 重新hash
                        newTab[e.hash & (newCap - 1)] = e;
                    }else if (e instanceof TreeNode){
                        //移动红黑树节点
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    }else {
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                // 如果hash按位与旧容量值为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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }

链表树化

当hash表中某个桶内的链表节点过多时需要将链表转换为红黑树以提高查询效率如在HashMap#putVal方法中:

    //该循环体逻辑为插入节点数据时在hash表某个桶上的链表知道key值相同的节点或者找到节点末尾空位
    //binCount当前链表节点数量
    //TREEIFY_THRESHOLD链表树化阈值
    for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
            //在链表末尾添加节点
            p.next = newNode(hash, key, value, null);
            //链表中节点数量大于树化阈值,
            if (binCount >= TREEIFY_THRESHOLD - 1) {
                treeifyBin(tab, hash);
            }
            break;
        }
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
            break;
        }
        p = e;
    }

在向hash表中插入数据的时候如果插入位置是同一个桶的链表那么会根据链表树化阈值判断是否需要进行树化 树化方法为HashMap#treeifyBin在树化方法中还要先判断hash表容量是否大于最小树化表容量默认为64 否则不会进行树化,而是进行表扩容。

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 如果表容量小于最小树化表容量,那么会先扩容,不会进行树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //重新hash扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }