Java集合源码学习之HashMap

HashMap底层是通过动态数组+链表(或红黑树),具有以下特点:

  • 数组的动态扩容保证
  • 链表与红黑树的转化
  • 每一个存储的K-V对象都是一个Map.Entry<K,V>对象
    在这里插入图片描述
    红黑树

红黑树是一种特殊的平衡二叉树(AVL)。红黑树在插入和删除上比平衡二叉树效率高;在数据的查询上,由于可能存在的树的高度比AVL树高一层,查询性能略差。红黑树具有以下特点:

  • 每一个节点都有一个标志位标识,或者是黑色,或者是红色
  • 根节点一定是黑色
  • 每个叶子节点是黑色
  • 如果一个节点是黑色,它的子节点一定是红色
  • 从一个节点到该节点的子孙节点的所有路径上,包含相同数目的黑色节点
    HashMap的重要属性
// 初始默认容量,必须是2的常数幂
static final int DEFAULT_INITIAL_CAPACITY = 1<<<4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转化为树的临界值
static final int TREEIFY_THRESHOLD = 8;
// 树转化为链表的临界值
static final int UNTREEIFY_THRESHOLD = 6;
// 最小的树容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 元素存储数组,第一次使用时初始化
transient Node<K,V>[] table;
// 键集合
transient Set<Map.Entry<K,V>> entrySet;
// map中元素个数
transient int size;
// 调整容量的临界值 (capacity * load factor)
int threshold;
// 加载因子,创建时不传入,默认DEFAULT_LOADER_FACTOR
final float loadFactor;
// 静态内部类,单项链表
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);
     }
	// 替换Value,返回Old
     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;
     }
 }

HashMap 中下标计算

(1)计算哈希值,key的哈希值与高低16位进行异或运算

// 将Key的哈希值进行高低16位的异或运算
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么需要进行高低16位的异或运算?

HashMap在进行下标计算时,采用公式:hash&(length-1),即上述方法计算的哈希值hash与length-1进行"与运算"。

通常情况下,我们在使用Map时,绝大多数情况下,map中的数据不是很多,一般小于2^16即65536.所以h&(length-1)始终是h的低16位参与运算,高16位始终处于浪费状态。而且当key.hashCode()得到的数值低16位相同,高16位差异时,更容易出现哈希冲突。例如:

(1) key.hashCode()直接与(length-1)进行& 运算
length=16,(length-1)=15,转化为二进制: 0000 1111
key.hashCode()的数值为236,829,409,转化为二进制:0000 1110 0001 1100 1011 1010 1110 0001
进行&运算
0000 1110 0001 1101 1011 1010 1110 0001
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0001
计算之后,获得下标为1
假如key.hashCode() = 236,829,425  
0000 1110 0001 1101 1011 1010 1111 0001
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 0001
下标仍然是1
(2)key.hashCode()先进行高低位异或运算,再进行&运算
key.hashCode()=236,829,409,高低16位进行异或运算
0000 1110 0001 1101 1011 1010 1110 0001
^
0000 0000 0000 0000 0000 1110 0001 1101
……………………………………………………………………………………………………………………
0000 1110 0001 1100 1011 0100 1111 1101
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 1101
下标位:13
假如key.hashCode() = 236,829,425 
0000 1110 0001 1101 1011 1010 1111 0001
^
0000 0000 0000 0000 0000 1110 0001 1101
…………………………………………………………………………………………………………………………
0000 1110 0001 1100 1011 0100 1110 1100
&
0000 0000 0000 0000 0000 0000 0000 1111
……………………………………………………………………………………………………………………
0000 0000 0000 0000 0000 0000 0000 1100
下标位:12


当key.hashCode()先进行高低16位异或运算之后,可以让高16位参与后续的与运算,可以减少哈希冲突,下标更加散列。

为什么用^而不是用&或者|运算?

因为 & 或者 | 运算,均偏向于0或者1,并不是均匀的概念,而^是去除相同取差异,会更加随机

&运算:0:75%   1:25%
|运算:0:25%   1:75%
^运算:0:50%   1:50%

补充

  • 当length = 8时,下标计算结果取决于哈希值的低3位
  • 当length = 16时,下标计算结果取决于哈希值的低4位
  • 当length = 32时,下标计算结果取决于哈希值的低5位
  • 当length = 2^n时,下标计算结果取决于哈希值的低n位

源码分析之构造函数

// 1、无参构造函数,构建一个空的HashMap,默认初始容量为16,加载因子为0.75f。
// 此时变量table并未实例化
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 2、构建一个空的HashMap,指定初始容量,加载因子默认为0.75f
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 3、Map构建
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            // 长度除以负载因子,可能会出现小数,+1 是为了下一步向上取整
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}
// 4、创建一个指定初始容量和加载因子的空HashMap
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);
}
// 返回不小于给定数据且距离最近的2的N次幂
static final int tableSizeFor(int cap) {
    // 为了排除本身为2的N次幂,例如 cap = 8,若不进行减一,计算后获得的是16
    int n = cap - 1;
    // 以下做法是为了将最高位1之后的所有数字变为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;
}

tableSizeFor 算法举例
例如cap = 9,则n =8,转为二进制:0000 0000 0000 0000 0000 0000 0000 1000
n >>> 1: 0000 0000 0000 0000 0000 0000 0000 0100
n |= n >>> 1:
  0000 0000 0000 0000 0000 0000 0000 1000
|
  0000 0000 0000 0000 0000 0000 0000 0100
  ……………………………………………………………………………………………………………………………………
  0000 0000 0000 0000 0000 0000 0000 1100 
n|=n >>> 2:
   0000 0000 0000 0000 0000 0000 0000 1100 
  |
   0000 0000 0000 0000 0000 0000 0000 0011
  ……………………………………………………………………………………………………………………………………
   0000 0000 0000 0000 0000 0000 0000 1111
  之后的右移4位、8位、16位,均为0.
  

思考:为什么必须是2的N次幂?

源码分析之PUT

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
// 通过高低16位异或运算,更加随机、散列
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
	* Implements Map.put and related methods
    * @param hash key.hashCode()高低16位异或运算之后 的哈希值
    * @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) {
    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, i;
    // 当table数组为为空时,初始化table。即第一次添加数据时,对HashMap进行初始化
    if ((tab = table) == null || (n = tab.length) == 0){
        n = (tab = resize()).length;
    }
    // i =(n-1) & hash,计算下标位。即当table数组中,i下标数据为null时,直接创建节点
    // 若不为null,则确定索引位置,则进行赋值
    if ((p = tab[i = (n - 1) & hash]) == null){
        tab[i] = newNode(hash, key, value, null);
    } else {
        Node<K,V> e;
        K k;
        // 若hash、key、以及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);
        }
        // 否则,确定了索引位置,但是链表的第一个节点,不能与key进行匹配,需要对链表进行循环
        else {
            // 循环链表
            for (int binCount = 0; ; ++binCount) {
                // 若节点的下一个节点为null,则直接创建新的节点,放在该节点之后,即next
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 判断节点数量是否大于“树化”临界值,若大于临界值,需要将链表转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                // 若完全匹配,则跳出循环,说明key对应的节点已经存在,则返回节点
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
                    break;
                }
                p = e;
            }
        }
        // 节点存在,根据条件,对value进行赋值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 将原值进行覆盖
            if (!onlyIfAbsent || oldValue == null){
                e.value = value;
            }
            // 空函数,LinkedHashMap使用
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 赋值操作后,集合操作数+1
    ++modCount;
    // 当前集合元素加1,并判断是否需要扩容
    if (++size > threshold){
        resize();
    }
    // 空函数,LinkedHashMap实现
    afterNodeInsertion(evict);
    return null;
}
// 初始化数组及后续扩容
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
        }
    // 原table数组为空,但阈值不为空时,对newCap进行赋值
    // 场景:HashMap的构造方式有4种,当采用第2、3种方式构建时,传入了initialCapacity,
    //这时table并没有初始化,但经过tableSizeFor(initialCapacity)方法,threshold已经被赋值
    }else if (oldThr > 0){ 
        newCap = oldThr;
    // 默认初始化
    }else { 
        // newCap = 16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // newThr = 16*0.75 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 第2、3种构造方法创建的HashMap
    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;
    // 原数组不为空,需要将元素转移,否则直接返回newTab
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 判断节点是否为空,并赋值给e
            if ((e = oldTab[j]) != null) {
                // 将原数组节点置为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;
                       // 判断扩容后,高位是否为0,如果为0,则下标不变,若不为0,下标=原下标+原数组长度
                        // 不需要再次计算hash
                        // 这也是每次扩容,均为2的N次幂的原因
                        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;
}

下面以oldCap=16进行举例,则newCap = 32.下标计算为 hash&(length-1)

hash&length ==0时,扩容后,元素的下标不变;

length=16,hash的数值为236,829,411,转为二进制:0000 1110 0001 1101 1011 1010 1111 0001
计算下标:
 0000 1110 0001 1101 1011 1010 1110 0011
&
 0000 0000 0000 0000 0000 0000 0000 1111
 ………………………………………………………………………………………………………………
 0000 0000 0000 0000 0000 0000 0000 0011  ->下标位置为3
 
 hash & length:
  0000 1110 0001 1101 1011 1010 1110 0011
 &
  0000 0000 0000 0000 0000 0000 0001 0000
  …………………………………………………………………………………………………………………………
  0000 0000 0000 0000 0000 0000 0000 0000  ->0,下标位置不变
 
 验证:当扩容至32时,进行下标计算:
  0000 1110 0001 1101 1011 1010 1110 0011
 &
  0000 0000 0000 0000 0000 0000 0001 1111
  ………………………………………………………………………………………………………………
  0000 0000 0000 0000 0000 0000 0000 0011  ->下标为3

hash&length !=0时,扩容后,元素的下标new = old + lenght;

length=16,hash的数值为236,829,425,转为二进制:0000 1110 0001 1101 1011 1010 1111 0001
计算下标:
 0000 1110 0001 1101 1011 1010 1111 0001
&
 0000 0000 0000 0000 0000 0000 0000 1111
 ………………………………………………………………………………………………………………
 0000 0000 0000 0000 0000 0000 0000 0001  ->下标位置为1
 
 hash & length:
  0000 1110 0001 1101 1011 1010 1111 0001
 &
  0000 0000 0000 0000 0000 0000 0001 0000
  …………………………………………………………………………………………………………………………
  0000 0000 0000 0000 0000 0000 0001 0000  ->不为0,则扩容至32时,下标为1+16 = 17.
 
 验证:当扩容至32时,进行下标计算:
  0000 1110 0001 1101 1011 1010 1111 0001
 &
  0000 0000 0000 0000 0000 0000 0001 1111
  ………………………………………………………………………………………………………………
  0000 0000 0000 0000 0000 0000 0001 0001  ->下标为17

PUT方法逻辑图:
在这里插入图片描述
回顾之前问题,为什么长度是2的N次幂?

  • 计算出来的下标更加散列。下标计算i时,先计算hash=key.hashCode()^(key.hashCode()>>>16),

    i = hash&(length-1),table长度是2的N次幂,可使length-1低位均为1,排除长度的过分干扰。

  • 扩容后,数据的移位更加简单有效。由于长度是2的N次幂。数组扩容后,节点是链表或者红黑树的前提下,只需要考虑hash&length是否为0,若为零,则下标不变;若不为零,则new = old +oldLength

源码分析之GET

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 通过hash&(length-1)定位下标,若不为Null,进行key判断
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 
    Node<K,V> first, e; 
    int n; K k;
    // 如果table不为Null、table中含有元素、且计算出的下标不为null,
    // 否则直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果第一个节点的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;
}

GET之逻辑图
在这里插入图片描述

注:以上仅为个人学习记录,错误之处,请指正。
另:若有侵权,请告知。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值