HashMap源码解析

之前研究过几次源码,通过看资料等各种方式,搬着源码一起看,最近对写代码这件事情有了新的认识,所以打算从另一个角度,去熟悉之前做的事儿;

此次打算写一个全面的源码解析,深入到源码中的构造函数–增删改查的方法;希望能在这个过程中,有比之前更深入的了解。

本文的源码是java11版本的HashMap

  1. 关于HashMap的简介:
  • HashMap是一个关联数组、哈希表,它内部没有进行并发处理,所以是线程不安全的,允许key和value为null,无序集合;
  • 它的底层结构是数组➕链表的形式,这个数组称之为哈希桶,每个桶节点中存储的是key的哈希值相同的键值对;JDK8对hashMap的结构进行了优化,当每个哈希桶存储的链表长度大于8的时候,就转换成红黑树存储,这样无论是插入还是查找,都提高了效率;
  • 因为hashMap的底层是数组,就也涉及到了扩容的问题。当HashMap的容量到达threshould的阈值时,就会进行扩容。扩容后的容量是扩容为大于当前容量的2的N次方。这样根据key的哈希值寻找对应的哈希桶时,可以使用位运算代替取余。
  • 根据key的哈希值确定元素在桶中的位置—这里的key的哈希值并不仅仅是key的hashCode的值,还会经过扰动函数的扰动,最后得到。因为key的hashCode是一个int值,其取值范围是40多亿,但是hashMap的默认长度Hi只有16,所以要进行扰动;
  • 扰动函数的原理:为了解决hash碰撞,************** 待补充 ************
  • 扩容操作的过程:每次的扩容,都会先新new一个Node数组作为哈希桶,然后将老得数组中的所有元素放入新的桶中,相当于重新给每一个元素进行put操作,所以扩容很消耗性能,hashMap的容量越大,则扩容消耗的性能就越大;

2. 源码解析

下面是一些常量说明:
// 默认容量必须是2的幂次
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
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;

//---------fields
// 整个HashMap中的桶数组,初始化的时候容量设置为2的幂次,这里也容忍一些长度定义为0的操作;
transient Node<K,V>[] table;
// 主要用作 keyset()和values;
 transient Set<Map.Entry<K,V>> entrySet;
//HashMap中存储的键值对的数量
transient int size;
//扩容阈值,当size>=threshold时,就会扩容
int threshold;
//HashMap的加载因子
final float loadFactor;
//这个字段主要记录的是hashMap结构变化的次数(eg:rehash),主要用于迭代的时候,HashMap是一个“快速失败”的集合类型,即:当迭代的时候,发现当前的map对象倍修改了,会立即抛异常ConcurrentModificationException,不再继续往下执行;
transient int modCount;



关于每个桶节点对应的链表中节点的源码Node解析[HashMap内部定义的单向链表]:
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;  //key的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; }
        //Node重写了toString方法,key=value;
        public final String toString() { return key + "=" + value; }
		// todo:这里不太懂 这个hashCode方法的重写,是用来干嘛的
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

		//Node重写了equals方法,主要是用来在put方法中,当key的哈希值相同时,再顺着链表去查找是否有和当前的<key,value> 值完全相同的,如果没有则在链表尾部追加当前元素;
        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;
        }
    }

JDK7以及之前,每个桶节点都是只对应链表结构,JDK8开始,使用了红黑树,节点改为了treeNode;红黑树提高了查询效率;

	//计算key的哈希值的方法,当key为null的时候,返回0,所以hashMap中允许有一个key为null的键值对;
	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
hashMap的四个构造函数:
// 这个构造函数再研究一下
//判断自定义容量和负载因子,当自定义容量<0时和负载因子<0 则抛异常;如果自定义容量大于最大容量,则取最大容量作为初始化容量
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(int initialCapacity) {}
//该构造的所有fileds均为默认值
public HashMap() {}
//这是将一个Map对象直接作为初始化的参数,这时候会调用putMapEntries()方法,对桶的长度等fileds进行初始化设置;
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
//这里说一下最后一个构造函数,这里用到了一个很重要的方法:putVal();
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // 当table为null时,初始化容量
                float ft = ((float)s / loadFactor) + 1.0F;
                //size/负载因子+1的值和最大容量做比较,如果小于,则直接创建和map大小相同的即可,如果大于最大容量,则t=最大容量;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
                //如果t>扩容阈值,则阈值进行重新赋值;tableSizeFor方法,返回给定容量的两倍幂容量;
                if (t > threshold) threshold = tableSizeFor(t);
            } else if (s > threshold)
            	//当参数map的size大于最大容量,则进行resize  todo: 这个方法后面讲;
                resize();
            //在初始化容量,或者rehash之后,将m
            //todo: 这里没看懂。。。
            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);
            }
        }
    }

解析一下tableSizeFor()方法,这个方法主要是用来确定扩容之后的容量:

 	static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

或者

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;
}
//说一下这中实现:阈值 = loadFactor * 容量;
//通过这个方法,如果初始值为19,那么扩容后 数组的大小是多少呢? n=18(10010),n>>>1=01001(无符号右移),和n位或运算之后是11011;
// 这时候n=11011,   n>>>2=00110,和n位或运算之后=11111;    即32,2的5次方-1;
//即最后的扩容之后数组的大小,是比当前cap大的最小的2的幂次方;
下面上最重要的俩个方法 HashMap的put()方法:
	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * hashMap的hash方法
     * 如果key为null,则将哈希值设置为0;否则 取key的hashCode()  并将key的hashCode的值无符号右移16位,再和key的hashCode做 按位异或运算;
	 * 所以hashMap的key可以有一个为null; 而hashTable的源码中,是判断,如果value==null,直接空指针异常;然后key.hashCode(),如果此时key为null,则直接抛空指针;
     */
    static final int hash(Object key) {
	    int h;
	    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	}

说明:"^" 是按位异或运算符 ,是二元运算符,要化为二进制才能进行计算,在两个操作数中,如果两个相应的位相同,则运算结果为0,否则1;

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent 如果是true,是否需要替换相同的value值; 如果为true,表示不替换已经存在的value
     * @param evict   如果是false,则table正在被创建,表示数组是新增模式
     * @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;
        // 1. 如果tab还没有被初始化,先调用resize进行tab长度的确定;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
       // 2. 判断i = (n - 1) & hash]位置的桶节点是否为空:  如果为空的话,则new一个node;
       //    else  如果这个位置已经有了key-value对,则进行一些判断
       // 这里特别说明一下  index的算法:index 是利用 哈希值 & 哈希桶的长度----代替位运算;
        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 {
            // 否则按照链表的形式,插入这对数据.  java8是遍历链表,如果都不存在这个key,则在链表的尾部新增一个节点,
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表的长度>=红黑树的阈值-1,即当链表长度=8的时候就要进行链表--红黑树的转换;
                        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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //如果onlyIfAbsent为false或者当前节点的value=null的话,则将传入的key-value赋值进来;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 如果当前hashMap的长度大于了阈值,则进行resize操作;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

当长度>=8的时候,链表是怎么转为红黑树的呢?

  • [ 这里还是没怎么看懂,怎么从链表转为红黑树的?]
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 当map的桶数组为空或者容量 小于 链表转为红黑树的最小容量时  初始化数组
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //取下标  index = (n-1)& hash,
        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);
        }
    }
下面介绍一下HashMap的get方法():
  • [key的hash是怎么实现的,hash(key)]
 	public V get(Object key) {
        Node<K,V> e;
        // 返回e.getval()
        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;
        // 当map桶不为空 且第一个节点也不为null的时候  (  第一个节点的下标=(n - 1) & hash  )
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        	// always check first node   首先先查询第一个节点的key是不是等于当前的key   (等于的判断方式 1.节点的hash相等,节点的key和参数key相等  3.参数key和节点key的equal为true)
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
            	// 如果第一个节点不等于参数key ,则hasNext ,
            	// 第一个node如果是红黑树类型,则按照红黑树来进行查询
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
               	// 否则就是链表,do while{}, 当e的下一个节点不为null的时候,则去对比 节点的hash key的== 和 equal是否为true;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }


/**
* 红黑树的查找方式
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//使用红黑树的根节点去调用find,找到key对应的节点;    root.(hash,key,null)
    return ((parent != null) ? root() : this).find(h, k, null);
}

//getTreeNode调用的俩方法是 静态内部类 static final class TreeNode <K,V> extends LinkedHashMap.Entry<K,V> (){}中的俩个方法
final TreeNode<K,V> root() {
// 循环去找根节点    根节点的判断   r.parent = null;
  	for (TreeNode<K,V> r = this, p;;) {
        if ((p = r.parent) == null)
            return r;
        r = p;
    }
}
       
//从红黑树的根节点开始去查询与key对应的节点;

 - […………………………^_^] 这里的 p=thisthis是当前节点么?

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                // 如果当前节点的hash值大于传进来的hash值,则传进来的key在当前节点的左边;   p赋值为左边的节点
                if ((ph = p.hash) > h)    p = pl;
                // 如果 当前节点的hash小于传进来的hash,则要查询的在当前节点的右边;    p赋值为右边的节点
                else if (ph < h)    p = pr;
                // 如果当前节点的key和key相等,且equal也为true  则当前节点就是要查询的节点;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))   return p;
                // 如果pl或者pr都为null,则赋值给相反方
                else if (pl == null)   p = pr;
                else if (pr == null)   p = pl;
                else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                // 如果上面的条件都不满足:  手动将右节点赋值给p,调用find去迭代直至找到q;
                else if ((q = pr.find(h, k, kc)) != null)  return q;
                // 如果上面的条件都不满足: 将左节点赋值给p,去左dowhile循环;
                else  p = pl;
            } while (p != null);
            
            return null;
        }

get()方法逻辑总结
通过key的hash值和key去找node;
当map桶数组不为空的时候,且第一个桶节点也不null的时候,
首先一定先单独对比第一个first是不是当前要找的key; 如果是则直接返回;
如果不是,则判断当第一个节点 first.next 是否等于null;
如果等于null 则直接返回null 没找到key对应的值;
如果不等于null, 判断下一个节点是不是红黑树,如果是红黑树 则按照红黑树的方式查找; 否则,按照链表的方式查找;

链表的查找方式:
比较简单: 使用do while{}, 当e的下一个节点不为null的时候,则去对比 节点的hash key的== 和 equal是否为true;

红黑树的查找方式:
先通过root()方法找到当前红黑树的根节点; ( 循环去找根节点 根节点的判断 r.parent = null; )
再使用根节点 调用find方法,去查找;
find()方法的原理: 也是使用do…while{} 循环匹配查找
第一二步: 通过当前节点的hash值和传进来的参数hash值做对比,如果小于参数hash,要命要找的对象在当前节点p的右边,则设置 p=pr; 反之 设置为 p=pl;
第三步: 不大于不小于,那就是等于咯,这时候去判断当前的key的 == 和 equeal是不是都相等,如果相等的话,则返回当前节点;
第四步: 如果都不是,则去判断,如果p的做节点为null,则证明当前节点已经是左边的节点,没有比他更小的了,则p=pr; 反之p=pl;
第五步: 如果上面的四个步骤的if条件都不满足,则 考虑使用迭代 强制 对赋值pr pl 进行迭代查找, 这里的迭代很有意思,pr是作为一步骤,直接通过pr.find() 迭代进行查找,知道找到,返回节点值; pl呢则是以上所有的条件都不满足的时候,将p=pl; 这样pl进行do…while{}循环查找;

下面来看看 hashMap的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;
        // 还是先判断桶数组 和 第一个节点是不是null;
        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 (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            // 和get一样,分别判断 通过红黑树和链表的查找方式 先找到对应的节点node;
            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不等于null,且node的value和参数的value也相同的时候, 分别以红黑树或者链表的形式,一处节点,并将modCount+1,size-1;
            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);
                //如果是当前节点,则将现在的table的index的位置赋值给 当前节点node的next指针,(即将node移除到链表了)
                else if (node == p)
                    tab[index] = node.next;
                else
                	//如果以上都不符合,就直接将node.next赋值给p的next,移除了node节点
                    p.next = node.next;
                //修改计数加1,桶的size减一
                ++modCount;
                --size;
                //
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

remove()方法总结:
首先 第一步和查找的逻辑一样,根据key的hash值和key去在红黑树或者链表中查出对应的节点 (依旧先查询第一个node节点是不是当前节点,然后判断是红黑树还是链表,再按照各自的方式查找);
第二步 移除。
1. 查找到的node的value也相等;判断如果是红黑树,则按照树的方式remove,负责按照链表remove;
2. 链表方式删除:如果是当前节点,则直接将index赋值给node.next节点; 否则:将node.next赋值给p.next

  • [ 这里的将node.next赋值给p.next 不太懂]
三、 相关的一些问题
  1. 为什么HashMap的阀值为0.75,指的是什么?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值