HashMap

特点

底层是数组+链表/红黑树,无序,非线程安全

  • key:可为null,不可重复,底层使用Set存储,所在类需重写equalshashcode
  • value:可为null,可重复,底层使用Collection存储,所在类需重写equals。
  • entry:底层使用Set存储,相当于 key-value 键值对。
  • HashMap实行了懒加载, 新建HashMap时不会对table进行赋值, 而是到第一次插入时, 进行resize时构建table;

成员变量

  • 默认初始容量(必须为2的整数幂)

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
  • 最大容量,当指定值较高将被其替换(必须为2的整数幂)

    static final int MAXIMUM_CAPACITY = 1 << 30;
    
  • 默认加载因子(未被指定时使用)

    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    

    为什么是0.75?
    ​ 根据统计学的结果, hash冲突是符合泊松分布的, 而冲突概率最小的是在7-8之间, 都小于百万分之一了; 所以HashMap.loadFactor选取只要在7-8之间的任意值即可, 但是为什么就选了3/4这个值, 我们看了HashMap的扩容机制也就知道了;

  • 树化阈值,当链表达到此值将转为树(至少为8)

    static final int TREEIFY_THRESHOLD = 8;
    
  • 链表阈值,当树容量小于此值转为链表

    static final int UNTREEIFY_THRESHOLD = 6;
    
  • 树化时数组最小容量,当即将树化时若数组未达到此阈值,将进行resize扩容

    static final int MIN_TREEIFY_CAPACITY = 64;
    
  • 存放数据的数组

    transient Node<K,V>[] table;
    
  • 保留缓存的entrySet()

    transient Set<Map.Entry<K,V>> entrySet;
    
  • 此映射中包含的键值映射数。

    transient int size;
    
  • 此HashMap在结构上被修改的次数结构修改是指那些更改HashMap中映射数量或以其他方式修改其内部结构(例如,重新刷新)的次数。

    transient int modCount;
    
  • 要调整大小的下一个大小值(容量*负载系数)。

    int threshold;
    
  • 加载因子

    final float loadFactor;
    

构造函数

无参

构造一个空集合,初始容量为16,加载因子为0.75

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

有参

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

常用方法

  • put(Object key,Object value):添加,key已存在则value覆盖

  • remove(Object key):删除

  • get(Object key):查询

    1. 首先获取当前key对应的数组索引位置,然后判断该位置的首节点是否是自己想要的值根据key和key.hashCode()来判断
    2. 首节点如果不是的话,判断节点是否是树节点,如果是的话,通过调用getTreeNode()来实现get()方法,如果不是树节点,那么就是链表,然后死循环遍历链表,查询是否有自己想要的值
    3. 如果上面的步骤都没有查询到数据,直接返回null.
    public V get(Object key) {
        //定义一个Node对象来接收
        Node<K,V> e;
        //调用getNode()方法,返回值赋值给e,如果取得的值为null,就返回null,否则就返回Node对象e的value值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
  • size():返回长度

  • keySet():遍历key

  • value():返回所有value

  • entrySet():返回所有键值对

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }
    

添加流程:

  1. 调用key所在类计算哈希值

内部类

Node

链表节点

TreeNode

树节点

Final方法

putTreeVal
  1. 从root节点开始寻找,

    若 预节点hash < 当前节点的 hash

    ​ 到左树寻找

    否则 预节点hash > 当前节点的 hash

    ​ 右树寻找

    否则 相同节点(同对象 或 同值) 直接返回。

  2. 若 hash相同 但是 equal不同

    ​ 若比较Comparable接口相同

    ​ 则在左右子树递归的寻找是否有与要插入的key equals相同的元素。如果有那么直接return返回。
    (也即是没实现Comparable接口,大小由hash判定。实现了,则由Comparable接口的比较方法判定)

  3. 如果遍历完所有的节点 并未找到equals相同的节点。那就需要插入该新节点。必须分出大小,所以通过执行tieBreakOrder方法,该方法的返回值是-1,1。如果是-1则插入到左边节点,1就插入到右边节点。

  4. 插入完成之后,需要重新移动root节点 到table数组的i位置的第一个节点上 并且需重新平衡红黑树。

final TreeNode<K,V> putTreeVal(HashMap<K,V> map/*当前Hashmap对象*/, Node<K,V>[] tab/*table数组*/,int h/*hash值*/, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;  	//标识是否被检索过
    TreeNode<K,V> root = (parent != null) ? root() : this; // 找到root根节点
    for (TreeNode<K,V> p = root;;) {	//从根节点开始遍历循环
        int dir, ph; K pk;
        // 根据hash值 判断方向
        if ((ph = p.hash) > h)
            // 大于放左侧
            dir = -1;
        else if (ph < h)
            // 小于放右侧
            dir = 1;
        // 如果key 相等  直接返回该节点的引用 外边的方法会对其value进行设置
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;		
        /**
			*下面的步骤主要就是当发生冲突 也就是hash相等的时候
			* 检验是否有hash相同 并且equals相同的节点。
			* 也就是检验该键是否存在与该树上
			*/
        //说明进入下面这个判断的条件是 hash相同 但是equal不同
        // 没有实现Comparable<C>接口 或者 (实现该接口 并且 k与pk Comparable比较结果相同)
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            //在左右子树递归的寻找 是否有key的hash相同  并且equals相同的节点
            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;
            }
            //说明红黑树中没有与之equals相等的  那就必须进行插入操作
            //打破平衡的方法的 分出大小 结果 只有-1 1 
            dir = tieBreakOrder(k, pk);
        }
        //下列操作进行插入节点
        //xp 保存当前节点
        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;
            //将root移到table数组的i 位置的第一个节点
            //插入操作过红黑树之后 重新调整平衡。
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

Static utilities

hash

计算hash值。

static final int hash(Object key) {
    int h;
    /*	if(key == null) 
    		return 0
    	else 
    		低16位与他的高16位做异或运算 
    		Tip:如果不这样的话,那么就只有hash()返回值的末x位参与到运算,这样就会造成hash冲突的概率高一些。如果先把key的hashCode()返回值的高16位和低16位进行异或运算,这样高16位也参与到hash()的运算逻辑了,这样就能减少冲突。*/
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

comparableClassFor

当在红黑树中添加节点时,会根据hash值进行比较,大右小左,但如果hash值相同呢?
这时还是会尝试使用compareComparables方法进行比较,但要保证参与比较的类实现了Comparable接口,此操作由本方法完成。

Eg:

入参出参状态
x implement Comparablex.classT
xnullF
StringString.classT
static Class<?> comparableClassFor(Object x) {
    /*if (类 x 实现了Comparable接口*/
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        /*if (类 x 是String类型)返回String.class*/
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        /*if (类 x 实现了接口)*/
        if ((ts = c.getGenericInterfaces()) != null) {
            for (int i = 0; i < ts.length; ++i) { //迭代接口
                //if (x.class实现的接口有参数(Eg:implement I<C>) && */
                //参数化类型的接口为Comparable类型(Eg:implement Comparable<C>)&&
                /*参数不为空 && 参数有一个 && 其为 x.class*/
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    //x implement Comparable<x>
                    return c;
            }
        }
    }
    return null;
}

compareComparables

当在红黑树中添加节点时,会根据hash值进行比较,大右小左,但如果hash值相同呢?这时还是会尝试使用compareComparables方法进行比较。

/**
  * @Param kc:kc Class对象
  * @Param k:当前节点对象
  * @Param x:新节点对象
  */
static int compareComparables(Class<?> kc, Object k, Object x) {
    /*
    if 新节点不为空 || 新节点的Class对象 与 kc 不同 
     	return 0;
    else 
    	return 新节点.compareTo(当前节点) Tip:a.compareTo(b)  === a - b
    */
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}

tableSizeFor

将传入容量转换为 2 的幂次方

static final int tableSizeFor(int cap) {//入参:148(10010100)
    int n = cap - 1;
    n |= n >>> 1; //n = n | (n >>> 1)	1xxxxxxx | 01xxxxxxx :11xxxxxx 128 + 64 + x = 192 + x
    n |= n >>> 2; //n = n | (n >>> 2)	11xxxxxx | 0011xxxx :1111xxxx  128 + 64 + 32 + 16 + x	= 240 + x
    n |= n >>> 4; //n = n | (n >>> 4)	1111xxxx | 00001111 :11111111  128 + 64 + 32 + 16 + 8 + 4 + 2 + 1	= 255
    n |= n >>> 8; //n = n | (n >>> 8)	11111111 |  :11111111  128 + 64 + 32 + 16 + 8 + 4 + 2 + 1	= 255
    n |= n >>> 16; //n = n | (n >>> 16) 11111111 |  :11111111	 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1	= 255
    /*
    if 255 < 0	
		return 1
	else
    	if 255 >= MAXIMUM_CAPACITY
			return MAXIMUM_CAPACITY
		else 
			return 255+1(出参:256(11111111 + 1) )
    */
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

Eg

入参:148(10010100) 出参:256(11111111 + 1)

Final方法

putMapEntities

将一个map赋值给新的HashMap

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取传入的map集合的大小
    int s = m.size();
    if (s > 0) {
        //如果HashMap没有被初始化
        if (this.table == null) {
            //将s除以负载因子+1可以得到HashMap所需的最大负载容量
            float ft = (float)s / this.loadFactor + 1.0F;
            //如果计算得到的最大负载容量大于最大值,则将t赋值为最大值
            int t = ft < 1.07374182E9F ? (int)ft : 1073741824;
            //如果t大于当前最大负载容量,则进行调整
            if (t > this.threshold) {
                this.threshold = tableSizeFor(t);
            }
        }
        //如果table已经被初始化且传入map的大小大于当前的最大负载容量则开始调整HashMap的大小
        else if (s > this.threshold) {
            this.resize();
        }
        //获取传入map的迭代器
        Iterator var8 = m.entrySet().iterator();
        //将map中的元素逐一添加到HashMap中
        while(var8.hasNext()) {
            Entry<? extends K, ? extends V> e = (Entry)var8.next();
            K key = e.getKey();
            V value = e.getValue();
            this.putVal(hash(key), key, value, false, evict);
        }
    }
}

resize

扩容

  1. 如果 table == null, 则为HashMap的初始化, 生成空table;
  2. 如果table不为空, 需要重新计算table的长度, newLength = oldLength << 1(注, 如果原oldLength已经到了上限, 则newLength = oldLength);
  3. 遍历oldTable
    1. 首节点为空, 本次循环结束;
    2. 无后续节点, 重新计算hash位, 本次循环结束;
    3. 当前是红黑树, 走红黑树的重定位;
    4. 当前是链表, JAVA7时还需要重新计算hash位, 但是JAVA8做了优化, 通过(e.hash & oldCap) == 0来判断是否需要移位; 如果为真则在原位不动, 否则需要移动到当前hash槽位 + oldCap的位置;
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) //如果头节点为红黑树类型,调用split
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 执行到这里,桶为链表
                    /*     正常情况下,计算节点在table中的下标的方法是:hash & (oldTable.length - 1)扩容之后,table长度
	                   翻倍,计算table下标的方法是hash & (newTable.length - 1)也就是hash & (oldTable.length * 2 - 1)
                       于是得出结论:新旧两次计算下标的结果,要么相同,要么是新下标等于旧下标加上旧数组的长度。*/
                    Node<K,V> loHead = null, loTail = null;/*下标不变*/
                    Node<K,V> hiHead = null, hiTail = null;/*下标改变*/
                    Node<K,V> next;
                    do {	//循环链表
                        next = e.next;	//为 next 赋值下一节点
                        if ((e.hash & oldCap) == 0) { //如果新链表最高位为0,下标不变
                            if (loTail == null) //尾空表示链表为空,e 做头
                                loHead = e;
                            else //链表不为空,e 置于最后
                                loTail.next = e;
                            loTail = e;	//e 做尾
                        }//下同
                        else {	//如果新链表最高位为1,下标改变
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null); //循环到null,循环链表结束
                    if (loTail != null) {	//下标不变链表
                        loTail.next = null;	
                        newTab[j] = loHead;	//直接将头节点置于新链表同下标
                    }
                    if (hiTail != null) {	//下标改变链表不为空
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;//直接将头节点置于新链表【旧下标 + 旧容量】
                    }
                }
            }
        }
    }
    return newTab;
}

putVal

  1. 如果key对应的索引位置是null,那么直接插入
  2. 数组里面key对应的索引值位置的值不为null,判断这个老值的key是否和新put的key是否相同,如果相同,就把老的值返回,并且记录这个位置
  3. 数组里面key对应的索引值位置的值不为null,判断这个索引位置的值是不是树结构,如果是树结构,调用树结构putTreeVal方法添加数据
  4. 数组里面key对应的索引值位置的值不为null,然后这个索引位置的值就是一个链表结构,然后遍历所有的链表(当遍历的长度大于8的时候,就会转成树结构),如果链表结构里面有key值和新key值相同,就把老的值给返回,并且记录这个位置,如果遍历到尾部还不相同,那么就使用尾插入把数据给添加进去。
  5. 对2步骤和4步骤记录的位置进行处理,一是把标记的位置的老值给返回,二是把新插入的值放到标记的位置上面。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果table为空,或者还没有元素时,扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果首结点值为空,则创建一个新的首结点。
    // 注意:(n - 1) & hash才是真正的hash值,也就是存储在table位置的index。在1.6中是封装成indexFor函数。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {    // 执行到这儿说明碰撞了
            Node<K,V> e; K k;
            // 如果在首结点与我们待插入的元素有相同的hash和key值,则先记录。
            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) {//#1 //下一节点为空,可以插入
                            p.next = newNode(hash, key, value, null);
                            // 当遍历的结点数目大于8时,则采取树化结构。
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                                break;
                        }
                        // 如果找到与我们待插入的元素具有相同的hash和key值的结点,则停止遍历。此时e已经记录了该结点
                        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;
                // onlyIfAbsent表示如果当前位置已存在一个值,是否替换,false是替换,true是不替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);  // 这个是空函数,可以由用户根据需要覆盖
                return oldValue;
            }
        }
    ++modCount;
    // 当结点数+1大于threshold时,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // 这个是空函数,可以由用户根据需要覆盖
    return null;
}

getNode

final Node<K,V> getNode(int hash, Object key) {
    //定义几个变量 
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //首先是判断数组table不能为空且长度要大于0,同时把数组长度tab.length赋值给n
    if ((tab = table) != null && (n = tab.length) > 0 &&
        //其次是通过[(n - 1) & hash]获取key对应的索引,同时数组中的这个索引要有值,然后赋值给first变量
        (first = tab[(n - 1) & hash]) != null) {
        //这个first其实就是链表头的节点了,接下来判断first的hash值是否等于传进来key的hash值
        if (first.hash == hash && 
            //再判断first的key值赋值给k变量,然后判断其是否等于key值,或者判断key不为null时,key和k变量的equals比较结果是否相等
            ((k = first.key) == key || (key != null && key.equals(k))))
            //如果满足上述条件的话,说明要找的就是first节点,直接返回
            return first;
        //走到这步,就说明要找的节点不是首节点,那就用first.next找它的后继节点 ,并赋值给e变量,在这个变量不为空时   
        if ((e = first.next) != null) {
            //如果首节点是树类型的,那么直接调用getTreeNode()方法去树里找
            if (first instanceof TreeNode)
                //这里就不跟进去了,获取树中对应key的节点后直接返回
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //走到这步说明结构还是链表    
            do {
                //这一步其实就是在链表中遍历节点,找到和传进来key相符合的节点,然后返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
                //获取e节点的后继节点,然后赋值给e,不为空则进入循环体  
            } while ((e = e.next) != null);
        }
    }
    //以上条件都不满足,说明没有该key对应的数据节点,返回null
    return null;
}

附录

Type(接口)

  • 原始类型
  • 参数化类型(ParameterizedType)
  • 数组类型
  • 类型变量

ParameterizedType(接口)

ParameterizedType extendsType

参数化类型

getActualTypeArguments

Type[] getActualTypeArguments()

返回实际类型的数组

Eg:

入参:Food<Fruit,Vegetable> 出参:[Fruit,Vegetable]

getRawType

Type getRawType()

该方法返回此类型的类或接口的类型

Eg:

入参:List 出参:List;入参:Map<String, Object> 出参:Map。

getOwnerType

Type getOwnerType()

返回一个Type类型对象,表示该类型所属的类型,必须至少有两个类型

Eg:

入参O<T>.I<S>,出参O<T>;入参:Map.Entry<String, Object> 出参:Map

getGenericInterfaces & getInterfaces

前者:获取由此对象表示的类或接口直接实现的接口的Type。

后者:获取由此对象表示的类或接口实现的接口

Eg:

class Food	interface Eat<T>	interface Run
class Dog implements Eat<Food>,Run

Type[] genericInterfaces = Dog.class.getGenericInterfaces(); 
//ParameterizedType Eat<Food>,Class Run

Class<?>[] interfaces = Dog.class.getInterfaces();
//Class Eat,Class Run

compareTo

比较两个对象

Eg:

a.compareTo(b) === a - b

红黑树结构

  1. 每个节点或为红,或为黑
  2. 根节点为黑色
  3. 每个叶子节点(NIL或NULL)为黑色
  4. 如果一个节点为红色,其子节点必须为黑色
  5. 从一个节点到任何子孙节点,路径上的黑节点相同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ETsx3zMm-1654504639264)(:/96b08573bea242bb9ec7a9a2e094696a)]

面试题

为什么HashMap 的长度为什么是2的幂次方?

​ 取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

HashMap里面的hash()返回值是key.hashCode() ^ (key.hashCode() >>> 16)的返回值呢?

为了减少hash的冲突概率。

​ 比如有两个key的hashCode()方法返回值分别如下

key1.hashCode(): 1111 1111 1111 1111 0101 0101 0111 0101
key2.hashCode(): 1111 1111 1110 1111 0101 0101 0111 0101

​ 如果没有^ (h >>> 16),二者在底层的数组索引值是:15 & hash:5

0000 0000 0000 0000 0000 0000 0000 1111

​ 这是因为二者低16位完全相同,然后15 & hash时,高位都是0,这就造成只有hash的低16位起了作用,而低16位完全相同,所以底层索引值也就相同了,这样很容易造成hash冲突。

但是如果有^ (h >>> 16)

就比如key.hashCode的值,也就是h变量如下所示:

1111 1111 1110 1111 0101 0101 0111 0101

高低16位参与运算,极大提高了hash的随机性,减少了hash冲突概率

Hashmap为什么引入链表?

​ hashmap的底层是数组,当map进行put()操作时候,会进行hash计算,判定这个对象属于数组的那个位置。当多个对象的值再同一个数组位置上面的时候,就会有hash冲突。这个时候就引入了链表

为什么jdk1.8会引入红黑树呢?

当链表长度大于8时,遍历查找效率较慢,故引入红黑树

​ 并不是只需要链表长度大于8,同时需要满足条件数组长度大于64的时候变成红黑树。还有如果红黑树的节点个数小于6的时候,红黑树还会变成链表

HashMap为什么一开始不就使用红黑树?

​ 因为红黑树相对于链表维护成本大,红黑树在插入新数据之后,可能会通过左旋、右旋、变色来保持平衡,造成维护成本过高,故链路较短时,不适合用红黑树。

HashMap的底层数组取值的时候,为什么不用取模,而是&?

​ tab[i = (n - 1) & hash]

因为在计算机运算的时候,使用&比取模的性能更快。

数组的长度为什么是2的次幂?

  • 为了让数据均匀分布,我们一般使用公式(hashCode % size)达到最大的平均分配。当容量为2的次幂,会满足一个公式:(n - 1) & hash = hash % n
  • &运算速度快,比% / 等常规操作快了十倍有余
  • 能保证 索引值 肯定在 capacity 中,不会超出数组长度

如果指定数组的长度不为 2次幂,就破坏了数组的长度是2次幂的这个规则吗?

不会的,HashMap 的 tableSizeFor 方法做了处理,能保证n永远都是2次幂。

多线程put并发的时候可能造成数据的丢失?

putVal方法中,假设两个线程执行添加,并同时执行到#1位置,前者将被覆盖

多线程put和get并发的时候,可能造成get为null?

线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。

JDK7 中 HashMap 因为头插入,导致get时出现死循环?

/**
  * Transfers all entries from current table to newTable. 
  */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {

        while(null != e) {
            //(关键代码)
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next; 
        } // while  
    }
}
  1. 有一数组容量为2,插入两个元素(a —> b)hash相同皆存入第一格
  2. 此时有两个线程A、B均执行put,存入第二格,扩容
  3. 两个线程分别创建新数组 A[],B[]
  4. 1.7 的 transfer 方法中有一关键代码:Entry<K,V> next = e.next;A线程执行到这里挂起,此时e为a,next为b
  5. B线程执行结束,并且a、b依旧冲突,位于新数组的第一格,因为头插,此时顺序为B[(b -> a),null]
  6. A线程继续执行,将 e = a 置于新数组[a, null]
  7. 第二次 e = b,但由于B线程已经修改了原表,此时 b.next = a
  8. 于是A线程的新数组为:A[(a <—> b),null],死循环

附录

【硬核】HashMap最全面试题(附答案)

结尾

HashMap的方法不是线程安全的。并发put操作时发生扩容,可能会导致节点丢失,产生环形链表等。 节点丢失,会导致数据不准 生成环形链表,会导致get()方法死循环。

由于扩容时使用头插法,在并发时可能会形成环状列表,导致死循环,在jdk1.8中改为尾插法,可以避免这种问题,但是依然避免不了节点丢失的问题。

HashMap的设计初衷就不是在并发情况下使用,如果有并发的场景,推荐使用ConcurrentHashMap

如有任何疑问,参照附录;如有必要,参照源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值