HashMap小结

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。

1.HashMap继承了AbstractMap类,实现了Map,Cloneavle,Serializable接口。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 

2.HashMap的初始容量为16。HashMap数组每一个元素的初始值都是Null,第一次调用put方法时,则会开始第一次初始化扩容,长度为16。。

 /**
   * The default initial capacity - MUST be a power of two.
   */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

3.HashMap的最大容量为1<<30。

/**
  * The maximum capacity, used if a higher value is implicitly specified
  * by either of the constructors with arguments.
  * MUST be a power of two <= 1<<30.
  */
    static final int MAXIMUM_CAPACITY = 1 << 30;

4.HashMap的装载因子为0.75。

  /**
    * The load factor used when none specified in constructor.
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

( H a s h 表 的 装 载 因 子 = 填 入 表 中 的 元 素 个 数 / H a s h 表 的 长 度 ) (Hash表的装载因子 = 填入表中的元素个数 / Hash表的长度) Hash=/Hash

当HashMap的装载因子大于该值,则进行扩容。

5.动态扩容机制

如下图中的散列表,当装载因子达到0.8时进行扩容,装载因子变为0.4,原来的数据就会存储在新的hash表中。

load_factor

当数据插入到HashMap时,如果装载因子还未达到临界值,此时还不需要扩容,插入的数据非常快,但如果装载因子达到了临界值,这是就需要先进行扩容,然后再插入数据,这个时候就会变得很慢。

当数据需要从HashMap中删除时,如果HashMap已经经历过扩容,随着数据的删除,空闲空间会越来越多。当程序对内存空间非常敏感时,可以设置当装载因子小于某个临界值时,启动动态缩容,让内容空间得到充分利用;当程序对内存空间不太敏感时,就不需要进行动态缩容处理。

​ 为了减少动态扩容耗时,可以将扩容的操作穿插在插入操作过程中。具体如下图所示:

动态扩容

这样每次插入时迁移一个数据,没有集中一次性迁移数据那样耗时,不会形成明显的阻塞。

由于迁移过程中,有新旧两个HashMap,查找数据时,先在新的HashMap中进行查找,如果没有,再去旧的HashMap中进行查找。

/**
     * 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() {
        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;
            }
            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
            newCap = oldThr;
        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"})
        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)
                        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;
                            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;
    }
  • 在resize()方法中,定义了oldCap参数,记录了原table的长度,定义了newCap参数,记录新table长度,newCap是oldCap长度的2倍(注释1),同时扩展点也乘2。

  • 注释2是循环原table,把原table中的每个链表中的每个元素放入新table。

  • 注释3,e.next==null,指的是链表中只有一个元素,所以直接把e放入新table,其中的e.hash & (newCap - 1)就是计算e在新table中的位置,和JDK1.7中的indexFor()方法是一回事。

  • 注释// preserve order,这个注释是源码自带的,这里定义了4个变量:loHead,loTail,hiHead,hiTail,看起来可能有点眼晕,其实这里体现了JDK1.8对于计算节点在table中下标的新思路:

    ​ 正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。

6.一个桶中bin(箱子)的存储方式由链表转换成树的阈值。即当桶中bin的数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8。

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;	

7.当执行resize操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6。

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

8.当桶中的bin被树化时最小的hash表容量。(如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作)这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

9.hash(Object key)原理

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

在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有,但原理没变。1.7源码在1.8中用tab[(n - 1) & hash]代替。

hashcode为int类型,4个字节32位,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。首先,为什么用异或计算?二进制位计算,a只可能为0,1,b只可能为0,1。a中0出现几率为1/2,1也是1/2,b同理。 位运算符有三种,|,&。a,b进行位运算,有4种可能 00,01,10,11 a或b计算结果为1的几率为3/4,0的几率为1/4,a与b计算结果为0的几率为3/4,1的几率为1/4,a异或b计算结果为1的几率为1/2,0的几率为1/2,所以,进行异或计算,得到的结果肯定更为平均,不会偏向0或者偏向1,更为散列。右移16位进行异或计算,将其拆分为两部分,前16位的异或运算,和后16位的异或运算,后16位的异或运算,即原hashcode后16位与原hashcode前16位进行异或计算,得出的结果,前16位和后16位都有参与其中,保证了32位全部进行计算。前16位的异或运算,即原hasecode前16位与0000 0000 0000 0000进行异或计算,结果只与前16位hashcode有关,同时异或计算,保证结果为0的几率为1/2,1的几率为1/2,也是平均的。所以为什么是右移16位,(1)结果的后16位保证了hashcode32位全部参与计算,也保证了0,1平均,散列性。(2)结果的前16位保证hashcode前16位了0,1平均散列性,附带hashcode前16位参与计算。(3) 16与16位数相同,利于计算,不需要补齐,移去位数数据更多情况,hashmap只会用到前16位(临时数据一般不会这么大),所以(1)占主因。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值