Java HashMap源码解读

1. HashMap底层数据结构

  1. 数组
  2. 链表(单向链表)
  3. 红黑树

2. 需要理解什么

  1. hash冲突如何解决
  2. 如何计算key的hash值
  3. 什么时候扩容、扩多大
  4. 数组的长度为什么是2的N次幂
  5. 查找、插入、扩容的过程

2.1 数据结构

在这里插入图片描述

  • 通过该图我们可以知道,当向map中插入一个值,首先是需要根据key生成一个hash值,根据hash值来确认存储在数组的什么位置。

    如果发生hash冲突就转换成以链表的形式存储,当链表的长度大于8并且数组的长度大于64时,转换成红黑树进行存储。

基本属性默认值:

// 默认容量大小 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表长度
static final int TREEIFY_THRESHOLD = 8;

// 最小转换为树的数组长度
static final int MIN_TREEIFY_CAPACITY = 64;

// Hash数组(在resize()中初始化)
transient Node<K,V>[] table;

// 容量阀值 计算规则 = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR 
int threshold;

table数组中存放的时Node对象,Node时HashMap的一个内部类,表示一个key-value

// Node对象 
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // hash值,根据key生成
        final K key; // key
        V value; // value
        Node<K,V> next; // 如果hash值在数组中存储着了,那么则生成链表,指针指向下一个

        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;
        }
    }

备注: 数组初始化不是在构造器中初始化,在resize(扩容)方法里面进行初始化的。

设置阀值

在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);
        // 负载因子 0.75f
    	this.loadFactor = loadFactor;
        // 计算阀值
        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;
    }

如何解决hash冲突

hashMap新增时,存储到指定的数组下标所在的位置,通过的是 hash & (length - 1),因此增加了hash的随机性,从而减少了hash冲突,归根结底,这些功劳都应该归根于设计时将长度设计为2的幂次方

扩容

hashMap扩容每次都是创建一个新的数组,把原数组中的内容重新映射到新数组上。

具体步骤:
在这里插入图片描述

根据流程图,可以知道:

  1. 首先判断table是否初始化,如果初始化了,则将容量,阀值都扩大到原来的2倍,这时,需要重新计算key-value在table中的位置。
  2. 如果没有初始化,并且我们的阀值是大于0的,那么说明了调用了hashMap的构造,传入的参数分别是初始容量大小,负载因子,经过计算阀值的方法,将阀值的大小作为容量的大小。

源码:

/*扩容*/
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    //1、若oldCap>0 说明hash数组table已被初始化
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }//按当前table数组长度的2倍进行扩容,阈值也变为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    //2、若数组未被初始化,而threshold>0说明调用了HashMap(initialCapacity)和HashMap(initialCapacity, loadFactor)构造器
    else if (oldThr > 0)
        newCap = oldThr;//新容量设为数组阈值
    else { //3、若table数组未被初始化,且threshold为0说明调用HashMap()构造方法
        newCap = DEFAULT_INITIAL_CAPACITY;//默认为16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75
    }

    //若计算过程中,阈值溢出归零,则按阈值公式重新计算
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    //创建新的hash数组,hash数组的初始化也是在这里完成的
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //如果旧的hash数组不为空,则遍历旧数组并映射到新的hash数组
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;//GC
                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 {
                    //rehash————>重新映射到新数组
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /*注意这里使用的是:e.hash & oldCap,若为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;
}

2.2. HASHMAP put方法

步骤解析:

  • table为空时,通过扩容的方式初始table的大小 (初始容量 * 0.75 = 实际容量大小)

  • 通过计算hash值求出下标后,如果没有存在hash冲入,则新增Node节点对象

  • 如若发生hash冲突,遍历链表查找需要插入的key是否存在,如果存在,则替换掉原来的value

  • 如若不存在,则在尾部进行插入,并且判断当前链表长度是否大于8,

  • 如若大于8,则将链表转换为红黑树进行存储

  • 判断key-value的数量是否大于等于阀值

  • 如若是,则进行扩容操作

    源码如下:

       public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
        // 执行操作的方法
    	 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
             // 1、 判断当前数组是否为空,如若为空,则调用resize方法,生成初始容量
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length; // 初始容量 ,计算: 默认容量 * 负载因子 = 阀值 (让阀值作为当前table的容量)
            if ((p = tab[i = (n - 1) & hash]) == null) // 数组中的下标位置: 计算, hash & (n-1) = p (指针)
                tab[i] = newNode(hash, key, value, null); // 如果为null,则生成一个新的Node节点
            else { // 如果不为null,插入的key-value已经存在
                Node<K,V> e; K k;
                // 如果第一个节点就是要插入key-value,则让p执行第一个节点
                if (p.hash == hash && 
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 如果p是TreeNode类型,则调用红黑树的的插入操作
                else if (p instanceof TreeNode) 
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    // 对链表进行遍历,用计算长度
                    for (int binCount = 0; ; ++binCount) { // 如果不等相等,则在链表的尾部插入Node
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // 判断链表长度是否大于等于8
                                treeifyBin(tab, hash); // 转换成红黑树存储数据
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                // 当前key存在
                if (e != null) { // existing mapping for key
                    V oldValue = e.value; // 用新值替换掉旧值
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
             // 判断当前容量是否达到阀值
            if (++size > threshold)
                resize(); // 扩容
            afterNodeInsertion(evict);
            return null;
        }
    
    

    2.3. HASHMAP GET方法

    步骤解析:

    • 根据key生成hash值,查找数据
    • 默认获取第一个Node,判断第一个Node节点中的key是否等于需要的key
    • 是,则返回,不是,则将指针下移
    • 判断当前Node是否为红黑树中的节点,如若是,则调用红黑树查找对应的Node
    • 如若不是,则向后进行遍历查询
    • 如若当前数组为null,则直接返回null

    源码解读:

    public V get(Object key) {
            Node<K,V> e;
        	// 根据key生成hash值
            return (e = getNode(hash(key), key)) == null ? null : e.value; 
        }
    
     final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; // 指向数组的hash数组
         	Node<K,V> first, e; // 指向hash数组中的第一个Node
         	int n; // 数组长度 
         	K k;
         	// 1、判断第一个节点Node中是否存在数据
            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;
                // 第一个key与查找的key不相等
                if ((e = first.next) != null) {
                    // 判断第一个Node是否为TreeNode类型的树据
                    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);
                }
            }
         // 不存在数据,返回null
            return null;
        }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值