hashMap

作者: 北海北_6dc3 | 来源:发表于2020-01-06 16:40 被阅读0次

参考资料:
吊打面试官
面试必备:HashMap源码解析(JDK8)
Java 8系列之重新认识HashMap
HashMap的loadFactor为什么是0.75
HashMap类的注释翻译

一、HashMap定义

  • HashMap实现接口Map,键值允许为null,HashMap和Hashtable相似【hashMap是非同步的,键值允许为null】
  • HashMap中get和put性能是恒定的。迭代器视图和容量有关,如果要使用迭代器,初始容量不宜设置过高(或加载因子过低)。
  • HashMap 中两个重要的参数,初始容量和负载因子,一般0.75是时间和空间比较折中的方案。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。初始值尽量考虑map的容量,减少刷新操作。
  • 如果存在大量键的hashcode相同,hashmap的性能会降低,可使用key实现
  • hashMap采用的是外部同步

二、源码分析

既然是hashmap,hash方法肯定是关键,我们首先分析hash

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

这里所有的键,通过hash方法做了一次处理,而不是直接使用键的hashcode。目的是因为,在查找数组下标时,使用了容量进行与运算,只有小于容量的二进制位会参与运算。通过将高16位与低16位参与运算,增加分布。

        n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)

参考资料:
JDK 源码中 HashMap 的 hash 方法原理是什么?-知乎

tableSizeFor:
this.threshold = tableSizeFor(initialCapacity);

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

获取容量,是容量成为2的倍数。
算法,整数32位,总存在一个最高位1,通过位移将1变为11然后在位移,变成1111,在变成11111111.
HashMap源码注解 之 静态工具方法hash()、tableSizeFor()(四)

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

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //tab数组,p每个桶
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    //tab为空创建tab
        if ((tab = table) == null || (n = tab.length) == 0)
            //resize进行扩容
            n = (tab = resize()).length;
    //index = (n - 1) & hash 下表位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            //创建一个新的Node元素
            tab[i] = newNode(hash, key, value, null);
        else {
            //指定位置已经有元素,也就是说产生hash碰撞
            Node<K,V> e; K k;
            //判断节点是否存在,覆盖原来原来的value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //判断是否是红黑树
                //是红黑树
                e = ((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);
                        //TREEIFY_THRESHOLD默认为8,大于8,转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //如果达到这个阈值转为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果节点key存在,则覆盖原来位置的key
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //检查e是否存在相应的key,如果存在就更新value,并且返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //判断hashmap是否需要resize扩容
        if (++size > threshold)
            resize();
        //留给子类LinkedHashMap来实现的
        afterNodeInsertion(evict);
        return null;
    }

逻辑还是比较简单,
1、不存在table或容量不足,重新扩容初始化。 resize();
2、hash对应数组位置为空,直接赋值
3、hash对应数组位置不为空,且节点为树,则直接调用 ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
4、hash对应数组位置不为空,则节点不为树,则遍历链表,如果相等,赋值,否则添加到尾节点。如果大于容量TREEIFY_THRESHOLD ,元素添加到尾节点,并转化为红黑树 treeifyBin(tab, hash);
5、如果结构变化,记录一次结构变化,结构变化定义

这个方法中,我们需要对几个方法进行进一步解释:

  • resize:扩容
  • putTreeVal:红黑树中放入节点
  • treeifyBin:链表转化为红黑树
resize
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;            //创建一个oldTab数组用于保存之前的数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;    //获取原来数组的长度
        int oldThr = threshold;                //原来数组扩容的临界值
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {    //如果原来的数组长度大于最大值(2^30)
                threshold = Integer.MAX_VALUE;    //扩容临界值提高到正无穷
                return oldTab;                    //返回原来的数组,也就是系统已经管不了了,随便你怎么玩吧
            }
            //else if((新数组newCap)长度乘2) < 最大值(2^30) && (原来的数组长度)>= 初始长度(2^4))
            //这个else if 中实际上就是咋判断新数组(此时刚创建还为空)和老数组的长度合法性,同时交代了,
            //我们扩容是以2^1为单位扩容的。下面的newThr(新数组的扩容临界值)一样,在原有临界值的基础上扩2^1
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0)
            newCap = oldThr;    //新数组的初始容量设置为老数组扩容的临界值
        else {               // 否则 oldThr == 0,零初始阈值表示使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;    //新数组初始容量设置为默认值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);        //计算默认容量下的阈值
        }
        if (newThr == 0) {    //如果newThr == 0,说明为上面 else if (oldThr > 0)
        //的情况(其他两种情况都对newThr的值做了改变),此时newCap = oldThr;
            float ft = (float)newCap * loadFactor;    //ft为临时变量,用于判断阈值的合法性
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);        //计算新的阈值
        }
        threshold = newThr; //改变threshold值为新的阈值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;    //改变table全局变量为,扩容后的newTable
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {    //遍历数组,将老数组(或者原来的桶)迁移到新的数组(新的桶)中
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {    //新建一个Node<K,V>类对象,用它来遍历整个数组
                    oldTab[j] = null;
                    if (e.next == null)
                                //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置,
                        newTab[e.hash & (newCap - 1)] = e;    //这个我们之前讲过,是一个取模操作
                    else if (e instanceof TreeNode)        //如果e已经是一个红黑树的元素,这个我们不展开讲
                        ((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;
                            //这个是新旧数组中,位置不变,原理:原始坐标计算方式:i = (oldCap - 1) & hash,
                           //那么新坐标应该是inew= (oldCap<<1 - 1) & hash,从二进制角度看,只有最高位 发生变化
                          //假设容量为16,hash二进制为0010 0110,老的位置应该是0010 0110&0000 1111=0000 0110
                          //现在 容量翻倍,新的位置0010 0110&0001 1111=0000 0110,所以只需要判定最高位是否为0即可,为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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

resize核心点在于,
1、hash的不变式:容量翻倍后,元素位置要么在原始位置n,要么在n+oldCap,即如下:

  if ((e.hash & oldCap) == 0) {

2、红黑树进行拆分重组

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
split方法
        /**
         * Splits nodes in a tree bin into lower and upper tree bins,
         * or untreeifies if now too small. Called only from resize;
         * see above discussion about split bits and indices.
         *
         * @param map the map
         * @param tab the table for recording bin heads
         * @param index the index of the table being split
         * @param bit the bit of hash to split on
         */
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            //原始位置链表
            TreeNode<K,V> loHead = null, loTail = null;
            //加长度bit位置链表
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            //循环链表
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //原始位置逻辑
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                //加bit位,位置逻辑
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                //链表逻辑
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified),红黑树生成逻辑
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

split方法还是比较简单的,通过next遍历树节点,生成原始位置和原始位置+bit位链表,判断元素是否需要链表化。
其中: if (hiHead != null) // (else is already treeified)
这段代码,判定很奇怪,思考?

treeify方法
        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //设置根节点黑色。
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                //非根节点
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //从根节点循环查找应插入的位置
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)   //通过hash判定为左子树
                            dir = -1;
                        else if (ph < h)         //通过hash判定为右子树
                            dir = 1;
                        else if ((kc == null &&    //hash相等,使用key判定
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);   //key也相等,使用类名判定

                        TreeNode<K,V> xp = p;
                        //赋值,并判定是否到达二叉树尾节点,如果是,则插入数据
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            //二叉树就行树平衡,二叉搜索树转红黑树,插入平衡
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

https://blog.csdn.net/weixin_42340670/article/details/80531795
treeify方法主要实现了二叉搜索树插入逻辑。并每次插入完成,进行红黑树平衡操作balanceInsertion。由于平衡操作后,根节点可能发生变化,所以根节点,需要重新赋值。

balanceInsertion方法
        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            //红黑树逻辑,插入节点均为红色,然后在进行颜色,位置调整
            x.red = true;
            //xp=父亲,xpp=祖父,xppl
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
                //红黑树无父节点,说明为根节点,根基点,直接图为黑色即可。
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                //父亲为黑色,添加一个红色,无影响
                //祖父为null,说明只有父节点,即父节点为根,为根必为黑色
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                //父亲是左子树,且为红色
                if (xp == (xppl = xpp.left)) {
                     //父亲的兄弟节点为红色。,一起变红为黑。祖父变红,以祖父为新节点,递归
                    if ((xppr = xpp.right) != null && xppr.red) {
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    //兄弟为黑色逻辑
                    else {
                         //左父亲的右孩子,需要旋转为左父亲的左孩子,父亲变为左孩子,右孩子变为父亲。所以添加值进行重定位为左孩子,即未旋转以前的父亲
                        if (x == xp.right) {
                            root = rotateLeft(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        //右旋进行平衡操作,父亲变为黑色,祖父表位红色,父亲变为顶点,祖父变为父亲的右孩子。进行下一次循环,发现父亲为黑色,完成操作。
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                //逻辑相反
                else {
                    if (xppl != null && xppl.red) {
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

红黑树平衡操作,参考维基百科https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91
简而言之,分为三种情况
1、新加入节点为根,直接变为黑色
2、新加入节点为黑色,不用做任何处理
3、父节点红色,有三种情况

  • uncle为红色,把父节点和uncle一起图黑,祖父节点图红,以祖父节点为标准进行下次判定
  • uncle为黑色,【内在逻辑,因为祖父和兄弟都是黑色,所以可以在祖父和兄弟之间插入一个红色,不影响红黑树平衡,所以需要进行旋转来实现】
moveRootToFront 方法

作用:Ensures that the given root is the first node of its bin.

        /**
         * Ensures that the given root is the first node of its bin.
         */
        static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            //判定存在
            if (root != null && tab != null && (n = tab.length) > 0) {
                int index = (n - 1) & root.hash;
                //获取bins中first node
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                //不相等,做处理,设置根为root,把root从链表中拿出来,root。prv.next=root.next,修改firt的prev
                if (root != first) {
                    Node<K,V> rn;
                    tab[index] = root;
                    TreeNode<K,V> rp = root.prev;
                    if ((rn = root.next) != null)
                        ((TreeNode<K,V>)rn).prev = rp;
                    if (rp != null)
                        rp.next = rn;
                    if (first != null)
                        first.prev = root;
                    root.next = first;
                    root.prev = null;
                }
                assert checkInvariants(root);
            }
        }

好的,到此,hashmap中的resize方法分析完成。
我们继续返回putVal中的剩下的两个方法。

putTreeVal方法
        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;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                //已经存在键,直接返回
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //hash相同,键不同,依然发生碰撞,
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        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);
                }

                  //已经到尾节点,赋值,并添加到红黑树,平衡,设置根节点
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.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;
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

逻辑就是放入hash中

treeifyBin方法
    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();
        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);
        }
    }

代码很简单,先组装TreeNode链表,然后树化 hd.treeify(tab);

get方法

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

    final Node<K,V> getNode(int hash, Object key) {
        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) {
            //hash相等,切key相等,直接返回,表示查找到
            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;
    }

查找逻辑先查找第一个元素是否相等,不等,则判定是链表还是红黑树,链表直接线性查找,红黑树进行二叉搜索树查找。

remove方法

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


    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //hash对应的bins里有值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //以下if和else逻辑同get
            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查找以后,准备进行处理
            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);
                //与链表第一个元素相等,如果只有一个,next=null,如果有多个,即指向第二个
                else if (node == p)
                    tab[index] = node.next;
                 //是链表,但不在第一个位置,则node是p的next,移除node,使p指向node.next.
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

移除方法,首先依然是查找元素,然后判定元素是数组,链表还是红黑树,进行相应处理。
红黑树调用方法:removeTreeNode

removeTreeNode方法

红黑树的删除,一堆if else,看的我蛋疼,说说原理,就是先转化为二叉搜索树删除元素,然后进行红黑树平衡操作balanceDeletion(root, replacement);。
红黑树相关内容,参考:

好的,至此,我们现在再回过头,来看第一篇参考文章,吊打面试官,看看能吊打了不
1、是链表的头尾插法问题,然后还涉及到了库容,解释这样做的原因是多线程才不会出问题,,excuse me???hashmap源码里连一个锁都没有,连cas都没有,这样的东西,能用到多线程中吗?是不是有点过度解读拟?把一个单线程的玩意,按照多线程来解读,还横纵向对比jdk7,说有坑,现在解决了
2、初始容量16问题
首先,如果手动设置容量会发现,进行了如下转换,即其实我们传入任何值,都可以,但是得明白,会转化成2的N次方

this.threshold = tableSizeFor(initialCapacity);

其实这个问题,就转化为为什么需要2的N次方,主要还是因为取索引代码

[i = (n - 1) & hash]

为了快速获取索引,我们只需要容量所有二进制为1,这样直接通过位运算,快速获取值,分布也均匀,如果扩容,位置只会是n或n+size,因为只有高位变化了1个

哟小家伙,知道的确实很多,那我问你个问题,为啥我们重写equals方法的时候需要重写hashCode方法呢?
你能用HashMap给我举个例子么?

其实这个问题,我们可以看object.hashCode定义

    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during
     *     an execution of a Java application, the {@code hashCode} method
     *     must consistently return the same integer, provided no information
     *     used in {@code equals} comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application.
     * <li>If two objects are equal according to the {@code equals(Object)}
     *     method, then calling the {@code hashCode} method on each of
     *     the two objects must produce the same integer result.
     * <li>It is <em>not</em> required that if two objects are unequal
     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
     *     method, then calling the {@code hashCode} method on each of the
     *     two objects must produce distinct integer results.  However, the
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java&trade; programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

总的来说,hashcode就是拿来支持hashtables的,方便key进行hash操作。
有以下要求:
1、equals相同,那么hashcode必须相同。
2、equals不同,那么hashcode可以相同。
3、如对象为键,那么为了实现hashtables高性能,那么最好,equals不同,hashcode不同。
从hashmap的角度来分析这个问题:
如果equals相同,但是hashcode不同,我就无法找到存入hash中的元素了
举例:
对象有个id属性,equals中判定,id相等,即为同一个对象,现在将对象放入hashmap中,过一段时间,我要获取它,重新构造了一个相同id的对象,但是hashcode不一样,我查找不到元素。获取失败。

相关文章

网友评论

      本文标题:hashMap

      本文链接:https://www.haomeiwen.com/subject/zdkooctx.html