说说Java中的HashMap

说起HashMap,大家可能都不陌生,因为它的使用场景很广泛。

今天让我们来追寻一下HashMap(JDK1.8)的实现,我采用源码加注释来解读,还请耐下心看下去。

一、基本参数

//默认的初始容量为16,而且容量必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量为230次方,如果你在构造方法中给了更大的值,则使用最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子0.75,为什么是0.75,是基于时间和空间的最佳值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//转换为红黑树的链表长度的阈值,也就是当链表长度大于等于8时,将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//将红黑树转换为链表的阈值,也就说当红黑树节点小于等于6时,将红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
//呵呵,这个不知道是啥
static final int MIN_TREEIFY_CAPACITY = 64;

二、构成hash表的节点

static class Node<K,V> implements Map.Entry<K,V> {
    //这个节点的Hash值,用于计算节点在hash表中的位置
    final int hash;
    //Map中的键
    final K key;
    //Map中的值
    V value;
    //基于链地址法解决哈希冲突的链表的下一个节点
    HashMap.Node<K,V> next;
    //构造方法
    Node(int hash, K key, V value, HashMap.Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}
三、成员变量
//存储元素的hashtransient HashMap.Node<K,V>[] table;
//包含每个entrySet集合,
transient Set<Map.Entry<K,V>> entrySet;
//包含的元素个数
transient int size;
//hash表的修改次数,这个计数只跟改变hash表结构的操作数相同
transient int modCount;
//Map要进行扩容的阈值
int threshold;
//哈希表的负载因子
final float loadFactor;

四、常用方法

1、构造方法

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

    构造方法没什么说的,要注意的是构造函数没有可以和Hash表有关的东西,所以这个时候Hash表是null。这个之后会说。

2、hash方法

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

    hash方法,求Key的hash值,将key的hashCode值和右移16位的HashCode值进行异或。只是为了将Hash值更加均匀,举个例子,假如有hashCode值    

    10 1110 0011 1010 1110 1001 原hashCode值

                                        10 1110 右移16位后的HashCode值

    10 1110 0011 1010 1100 0111 得到的Hash值,就是为了让hashCode值的后16位和前16位都加入到hash算法中。

3、get方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

    get方法很简单,知识简单的将key的hash值和key传给getNode方法,让getNode方法去判断。

4、getNode方法

final HashMap.Node<K,V> getNode(int hash, Object key) {
    //哈希表
    HashMap.Node<K,V>[] tab; 
    //first表示用hash值映射到哈希表中的那个元素,e是用来遍历可能存在的frist的后继节点
    HashMap.Node<K,V> first, e; 
    //哈希表的容量
    int n; 
    //用来表示firstkey    K k;
    //如果哈希表不为空  并且 哈希表长度大于0  并且根据传入hash值能找到相应的元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        //如果这个节点的hash值和hash值相同,并且k==keyk.equalskey,则返回该元素
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果还有后继节点的话,则遍历后继节点
        if ((e = first.next) != null) {
            //如果后继节点是树节点,说明链表转换为了红黑树,则去查找树节点的方法去查找
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
            //遍历链表
            do {
                //如果这个节点的hash值和hash值相同,并且k==keyk.equalskey,则返回该元素
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //根据hash值找不到相应的元素
    return null;
}

    如果你想自己了解源码,简易你将源码的某个方法拷到你的IDE,然后进行加注释理解,这样很有帮助。

5、containsKey方法

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

    这个方法非常简单,相当于调用get方法,如果get方法返回值非空,则说明包含该键,否则不包含

6、put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

    调用了putValue方法,传入键的hash值和键值对

7、putValue方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
    //哈希表
    HashMap.Node<K,V>[] tab;
    //要插入的节点
    HashMap.Node<K,V> p;
    //n为哈希表的长度  i是根据hash值计算出来的索引
    int n, i;
    //如果哈希表为空  或者  哈希表的长度等于0
    if ((tab = table) == null || (n = tab.length) == 0)
        //扩容哈希表
        n = (tab = resize()).length;
    //根据hash值计算出索引i所处的的位置若为null,则说明那个位置没有元素,则直接插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //说明根据hash值计算出索引i所处的的位置有元素
    else {
        //e为这个位置的原有元素
        HashMap.Node<K,V> e;
        //k为这个位置的原有元素的键
        K k;
        //如果要插入的元素的hash值,以及键与原有的元素的hash值和键相等,则进行替换
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果要插入的节点是树节点,则进行树节点的插入方法
        else if (p instanceof HashMap.TreeNode)
            e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //能来到这里,说明发生了哈希冲突,所以要将插入节点链入链表中
        else {
            //遍历链表
            for (int binCount = 0; ; ++binCount) {
                //后继节点为空
                if ((e = p.next) == null) {
                    //当节点插入到链表的末尾
                    p.next = newNode(hash, key, value, null);
                    //如果插入后链表长度大于8,则转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //后继节点的hash和键 与 插入节点的hash值以及键相同,则进行节点替换
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果插入或者替换成功,赋值或者更新值,将旧值返回
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //修改次数加1
    ++modCount;
    //如果哈希表中的元素大于要扩容的阈值,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

    要注意的是,在put的时候才会初始化哈希表。还有就是如果发生哈希冲突,是将节点插入到链表末尾,而不是像JDK1.7插入到链表头部。

8、resize方法

final HashMap.Node<K,V>[] resize() {
    HashMap.Node<K,V>[] oldTab = table;
    //获取哈希表的长度,两种情况,初次put时哈希表未初始化,长度为0,其他状况都是哈希表的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //原先的扩容阈值
    int oldThr = threshold;
    //初始化新的哈希表容量和新的扩容阈值
    int newCap, newThr = 0;
    //如果原哈希表长度大于0
    if (oldCap > 0) {
        //如果原哈希表长度大于等于哈希表最大长度
        if (oldCap >= MAXIMUM_CAPACITY) {
            //扩容不了了,只是将哈希表其他可用的位置都开放
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //哈希表容量减半,并且将扩容阈值也减半
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //如果原扩容阈值大于0,将哈希表的新容量设为原先的阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //这是初次初始化哈希表,将哈希表长度设为16,扩容阈值设为16*0.75 = 12
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    //更新扩容阈值,并初始化哈希表
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
    table = newTab;
    //如果不是初次扩容,也就是哈希表已经初始化过了
    if (oldTab != null) {
        //遍历原哈希表,进行再哈希
        for (int j = 0; j < oldCap; ++j) {
            //遍历到的节点
            HashMap.Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //链表只有一个节点,直接进行再哈希
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果是树节点,进行相应的树节点再哈希操作
                else if (e instanceof HashMap.TreeNode)
                    ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //说明这是至少包含两个节点的一个链表
                else { // preserve order
                    //ok,这里定义了两条链表,高位链表和低位链表
                    HashMap.Node<K,V> loHead = null, loTail = null;
                    HashMap.Node<K,V> hiHead = null, hiTail = null;
                    HashMap.Node<K,V> next;
                    do {
                        next = e.next;
                        //可以看出,低位链表保存原hash值与oldCap(不是oldCap-1)与运算为0的元素
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //高位链表保存原hash值与oldCap(不是oldCap-1)与运算不为0的元素
                        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;
                    }
                    //高位链表接保持原索引加上oldCap放入新哈希表中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
可以看出,原哈希表中的单节点链表直接进行再哈希放入新的哈希表,红黑树采用红黑树的方法,而多节点链表分为高位和低位链表。如果将多节点链表进行再哈希,此时可能会造成额外的时间和空间上的开销。

9、remove方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

    remove方法直接调用removeNode方法来进行节点的删除

10、remove方法

final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
    HashMap.Node<K,V>[] tab;
    HashMap.Node<K,V> p;
    int n, index;
    //根据传入的hash值可以在哈希表中找到相应的元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        //node表示当前要删除的节点,而e用来遍历链表每个节点
        HashMap.Node<K,V> node = null, e;
        K k;
        V v;
        //如果根据hash值和key找到了相应的元素,且元素在哈希表链表的头部
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            //如果是树节点,则在树中找该节点
            if (p instanceof HashMap.TreeNode)
                node = ((HashMap.TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                //在链表中寻找该节点
                do {
                    //如果找到了节点,则执行下一步,也就是删除
                    if (e.hash == hash &&
                            ((k = e.key) == key ||
                                    (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //如果找到的节点不为空
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            //如果是树节点,则进行树节点的删除
            if (node instanceof HashMap.TreeNode)
                ((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            //找到了,并且该元素是单节点链表
            else if (node == p)
                tab[index] = node.next;
            //找到了,是多节点链表
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

最后总结一下:

    1、JDK1.8的hashMap采用数组+链表+红黑树实现的

    2、为什么转换红黑树的阈值设为8,而转换链表的阈值设为6?

        只有长度N >= 7的时候,红黑树的平均查找长度lgN才会小于链表的平均查找长度N/2,这个可以画函数图来确定,lgN 与N/2的交点处N约为6.64。为什么设置为8而不是7呢?一个原因是为了防止出现频繁的链表与树的转换,当大于8的时候链表转红黑树,小于6的时候红黑树转链表,中间这段作为缓冲。
    3、JDK1.8的hashMap存在hash冲突时,是将冲突的节点放在链表的末尾,而JDK1.7是放在链表头部
    4、JDK1.8的hashMap在扩容时不会出现死循环,而JDK1.7的hashMap会出现死循环


猜你喜欢

转载自blog.csdn.net/yanghan1222/article/details/80189085