HashMap(jdk1.8)源码思想

HashMap实现于Map接口,元素以键值对的方式存储,允许使用null键和null值,不能保证元素的顺序,是线程不安全的。

HashMap-Hashtable

  • Hashtable的方法都有synchronized修饰,是线程安全的
  • HashMap可以使用null作为key,而Hashtable不允许

这里重点说一下hashtable不允许null的情况,在HashMap中,key为null的时候做了特殊处理,hash的时候直接返回0,也就是key为null时在数组的第1个位置。而Hashtable直接抛出了异常。为什么Hashtable要这样做呢?也做个特殊处理不香?

假设Hashtable允许key/value为null,当通过get(key)获取的返回结果是null时,我们无法判断这个key是否真的存在。我们需要调用containsKey方法来判断这个key是value=null还是这个key不存在。如果这个key存在,那么返回对应的value,不存在则返回null。

但是Hashtable是多线程的,当我们使用containsKey判断完key,然后再去get(key),在这期间可能有其他线程改变了数据map.put(key,null)。本来这个key是不存在的,应该返回null,但是现在这个key存在了,返回也是null。这样前后两次结果虽然一样,但实际上与真实情况不符。

数据结构

HashMap采用Entry数组来存储key-value值,每一个键值对组成了一个Entry实体,Entry类是一个单向的链表结构,这个链表就是用来解决Hash冲突的,在JDK1.8及以后链表还会树化形成红黑树。

源码分析

了解过HashMap的同学应该都知道,HashMap默认大小为16,由数组+链表组成,当有Hash冲突则会在冲突位置形成链表。以2的幂次进行扩容。那我们有没有想过,为什么是2的幂次扩容?

下面带注释的源码就不一一分析了,这里主要分析几个HashMap中的关键点:

key=null

当我们放入一个key为null的键到HashMap中时,HashMapa先在数组的第一个位置(可能是链表或树)查找是否有key=null的元素,如果找到,更新value。如果没找到,则添加到数组的第一个位置(可以理解为添加到链表的头部)。

什么时候扩容?

HashMap在什么时候会扩容,初始容量默认是16,这个值是2的次幂,就算我们自己指定一个非2次幂的值,HashMap也会自己计算出大于给定值的最近的2的次幂值(如果我们指定容量为7,HashMap会设置为8)。

HashMap中一个loadFactor变量,表示负载因子,默认为0.75,threshold = capacity(当前容量) * loadFactor。也就是容量使用超过75%就会进行扩容,并且每次扩容大小都是原先的2倍。

为什么是0.75就扩容?这里涉及到统计学和概率学,跟《泊松分布》有关,本人对这方面的知识一概不知,就不做讨论了。总之我们只要知道,超过0.75这个值的时候,Hash的冲突概率会大大增加,会导致冲突位置的链表过长,影响查找效率。

什么时候树化?

本人看过一些资料介绍的都是,当链表长度达到8就会树化,真的是这样吗?

/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 */
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)
        resize();
    // ......
}

我们看上面代码,还有一个判断(n = tab.length) < MIN_TREEIFY_CAPACITY,也就是数组长度小于64的情况下,是不会树化的,而是进行扩容操作。所以树化的是有两个条件的:

  • 链表长度达到8
  • 数组长度不小于64

那么为什么是8和64?

一些资料介绍都是针对的链表长度达到8这个条件,通过计算查找效率而得出8这个值效率更好,真的是这样吗?

* Implementation notes.
*  ......
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins.  In
* usages with well-distributed user hashCodes, tree bins are
* rarely used.  Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million

在HashMap源码中的Implementation notes注释处有这样一段描述,大致意思就是:当hashCode离散性很好的时候,树型bin用到的概率非常小,数据均匀分布在每个bin中,几乎不会有bin中链表长度达到阈值。但是在随机hashCode下,离散性可能会变差,而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。这里为什么是8,并不是一些资料所说的查找效率的原因,而是根据概率统计决定的。

怎么计算hash?

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

这里并不是普通的来计算hash值,而是让key的hash值的高16位也参与运算,减小hash的冲突。这样解释还是一脸蒙。我们来看个例子:

0b开头表示二进制
假设:key.hashCode() = 0b 0010 0101 1010 1100 0011 1111 0010 1110
那么:h = 0b 0010 0101 1010 1100 0011 1111 0010 1110
符号^ (异或:相同返回0,不同返回1)
h >>> 16 (h无符号右移16位,也就是低16位被去掉,前面补16个0)
h >>> 16的结果为:0b 0000 0000 0000 0000 0010 0101 1010 1100
与h进行^操作结果为:
0b 0010 0101 1010 1100 0011 1111 0010 1110
0b 0000 0000 0000 0000 0010 0101 1010 1100
0b 0010 0101 1010 1100 0001 1010 1000 0010(相同返回0,不同返回1)
HashMap计算下标公式:(len -1) & h.hashCode()
符号&(同为1返回1,其它返回0)
假设HashMap长度为16,16-1对应二进制为:0b 0000 0000 0000 0000 0000 0000 0000 1111
我们看到当长度较小时,二进制的高位都是0,这样在做&运算的时候,高位一直都是0,起作用的就是低位。
前面将key.hashCode()的高位与低位进行混淆,就是为了增加低位的随机性,从而在HashMap长度较小的时候降低hash冲突。

为什么是2次幂?

本人认为这里才是HashMap设计的高明之处,我们一起来慢慢分析:

对于HashMap如果是我自己计算key落在数组的哪个位置,我会用key.hashcode%array.length,这样计算的结果必定会在array.length这个范围内,但是HashMap中却是用(array.length-1) & key.hashcode。

  • 2^4=16,对应二进制:10000
  • 2^5=32,对应二进制:100000
  • 2^6=64,对应二进制:1000000

我们看到2的次幂的进进制结果都是整数,以1开头0结尾的数。以上长度-1得到的二进制结果分别为:1111、11111、111111。

假设key.hashcode的值为:0b10011011001,0b10011011001 % 0b10000=1001

我们用位运算&:0b10011011001 & 0b1111 = 1001

有没有发现什么规律?当长度是2的次幂时,这两种计算方法的结果是一样的。但是&运算比%运算要快,这样就提升了性能。如果长度为15,不是2的次幂,15二进制为1111,15-1二进制为1110。如果两个hash值分别为1111、1110,那么1111&1110就会和1110&1110相等,这样就增加了碰撞机率。所以说长度为2的次幂不仅提升的性能,同时也减少了hash的碰撞机率。

这样就完了吗?其实还有另一方面就是扩容后方便定位:

比如长度为16时,hash变量只有后4位会参与到运算,按照2位扩容后,也就是后5位会参与到运算。这个增加的第5位就是特殊元素,如果是0,那么之前的位置和扩容后的位置是一样的,如果是1,那么扩容后的位置就是(原位置索引+原数组大小)。

16扩容后为32
16-1的二进制为1111
32-1的二进制为11111
假设我们的key.hashcode的值为:0b 0100 1101 1001
0b 0100 1101 1001 & 0b 1111 = 1001(转换为10进制为9,也就是在数组9的位置)
0b 0100 1101 1001 & 0b 11111 = 11001(转换为10进制为25,也就是在数组25的位置25=9+16)
有没有发现什么规律?这里如果hashcode的第5位为0,那么扩容后原hashcode计算的下标和没扩容是一样的,这里第5位为1,计算的下标就为原位置+扩容大小。

根据这个规律,我们其实可以应用到数据库的扩容上面,这样可以避免数据库扩容后的数据迁移,具体以后讲数据存储的时候再说。

下面附上部分HashMap源码的部分注释

/**
* The default initial capacity - MUST be a power of two.
* 默认Table大小16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* MUST be a power of two <= 1<<30.
* Table数组最大长度
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 负载因子大小
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化值
* 链表长度超过8时,就会可能转换为树结构
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 树降级称为链表的值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当Hash表中的所有元素个数超过64时,才会允许树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 静态内部类
*/
static class Node<K,V> implements Map.Entry<K,V> {
    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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

/* ---------------- Static utilities -------------- */

/**
* 作用:让key的hash值的高16位也参与运算
* 假设:h = 0b 0010 0101 1010 1100 0011 1111 0010 1110
* 0b 0010 0101 1010 1100 0011 1111 0010 1110
* ^(异或:相同返回0,不同返回1)
* 0b 0000 0000 0000 0000 0010 0101 1010 1100(无符号右移16位,前面补16个0)
* =  0010 0101 1010 1100 0001 1010 1000 0010
*
* 计算index是通过(len -1) & h.hashCode()
* 在Table长度不是很长的时候,高16位也能参与运算,减少Hash冲突
* 通过无符号右移、异或运算后
*
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 哈希表
* 什么时候初始化?
*/
transient Node<K,V>[] table;
/**
     * The number of key-value mappings contained in this map.
     * 当前哈希表中元素个数
     */
transient int size;
/**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     * 当前哈希表结构修改次数
     */
transient int modCount;
/**
     * 扩容阀值,当你的哈希表中的元素超过阀值时,触发扩容
     */
int threshold;
/**
     * The load factor for the hash table.
     *
     * @serial
     * 负载因子
     * threshold = capacity * loadFactor
     */
final float loadFactor;
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //大小不能超过最大值,超过最大值自动设置为默认的最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //负载因子必需大于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    this.loadFactor = loadFactor;
    /*
        tableSizeFor
        返回一个大于等于当前值cap的一个数字,并且这个数字一定是2的次方数
         */
    this.threshold = tableSizeFor(initialCapacity);
}

final Node<K,V> getNode(int hash, Object key) {
    //tab:引用当前hashMap的散列表
    //first:桶位中的头元素
    //e:临时node元素
    //n:table数组长度
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //判断是否有数据
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //定位出来的桶位元素,即为我们要get的数据
        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 TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //桶位形成了链表
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    /*
        tab:引用当前hashMap的散列表
        p: 表示当前散列表的元素
        n: 表示散列表数组的长度
        i: 表示路由寻址结果
         */
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //第一次插入数据的时候进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    /*
        (n - 1) & hash 计算下标
        如果找到的位置刚好是null,这个时候,直接将当前k-v->node扔进去就可以了
         */
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        /*
            如果找到的下标有数据()
            e:不为null的话,找到了一个与当前要插入的key-value一致的key的元素
            k:表示临时的一个key
             */
        Node<K,V> e; K k;
        // 表示桶位中的该元素与你当前插入的元素的key完全一致,后续需要进行替换操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 表示桶位已经树化
        // 链表长度达到8,且所有元素超过64个
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 桶位是链表的情况,且链表的头元素与我们要插入的key不一致
            for (int binCount = 0; ; ++binCount) {
                //迭代到最后一个元素也没找到一个与你要插入的key一致的node
                //说明需要加入到当前链表的末尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //当前链表的长度,达到树化标准了,需要进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到相同元素的情况,需要进行替换操作
                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;
        }
    }
    //表示散列表结构被修改的次数,替换Node元素的Value不计数
    ++modCount;
    //插入新元素,size自增,如果自增后的值大于扩容阀值,则触发扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     *
     * 扩容
     * 为什么需要扩容?
     * 扩容就是把数组扩大,链表过长等都不利于查询
     * 解决哈希冲突导致的链化影响查询效率
     *
     */
final Node<K,V>[] resize() {
    //oldTab:引用扩容前的哈希表
    Node<K,V>[] oldTab = table;
    //oldCap:表示扩容之前table数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr:表示扩容之前的扩容阀值,触发本次扩容的阀值
    int oldThr = threshold;
    //newCap:扩容之后table数组的大小
    //newThr:扩容之后,下次触发扩容的条件
    int newCap, newThr = 0;
    //如果条件成立,说明hashMap中的散列表已经初始化过了,是一次正常扩容
    if (oldCap > 0) {
        //扩容之前的数组大小已经达到最大阀值,则不扩容。设置扩容条件为int最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //oldCap左移一位实现数值翻倍,并且赋值给newCap,newCap小于数组最大值限制且扩容之前的阀值>=16
        //这种情况下,下一次扩容的阀值等于当前阀值翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //oldCap==0,说明hashMap中的散列表是null
    //1、new HashMap(initCap, loadFactor);
    //2、new HashMap(initCap);
    //3、new HashMap(map);并且这个map有数据
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //oldCap==0,lodThr==0
    //new HashMap();
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //newThr为零时,通过newCap和loadFactor计算出一个newThr
    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"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //oldTab != null 说明本次扩容之前,table不为null
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            //当前node节点
            Node<K,V> e;
            //说明当前桶位中有数据,但是数据是单个数据,还是链表或红黑树并不知道
            if ((e = oldTab[j]) != null) {
                //方便JVM回收内存
                oldTab[j] = null;
                //当前桶位只有一个元素,从未发生过碰撞,直接计算出当前元素存放在新数组中的位置扔进去
                if (e.next == null)
                    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;
                    //01111,15的位置
                    //11111,31的位置
                    //15的位置链化后,只有两种可能,01111或11111
                    //01111的继续存放在15的位置,11111扩容后存放在31位置
                    //高位链表,存放在扩容之后的数组的下标位置为当前位置翻倍+1
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        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;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    //tab:引用当前hashMap中的散列表
    //p:当前node元素
    //n:表示散列表数组长度
    //index:表示寻址结果
    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) {
        //node:查找到的结果
        //e:当前Node的下一个元素
        Node<K,V> node = null, e; K k; V v;
        //当前桶位中的元素即为要删除的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        //当前桶位可能是链表也可能是树
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((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);
            }
        }
        //上面查找到的Node不为空则进行删除操作
        //matchValue用来判断Value的值,如果传了Value则会判断Value是否一致,一致则删除
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值