java集合框架----HashMap 源码分析

HashMap简述

HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作, 并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变

HashMap的属性

 /**
  *HashMap的默认初始容量,当使用默认的构造函数时,进行第一次扩容使用该值作为初始容量,必须是2的幂
  */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 /**
  *HashMap的最大容量,这里要与Integer.MAX_VALUE=(1<<31)-1;进行比较;
  *因为每次都是进行二倍扩容,所以当到大最大容量,再进行二倍扩容时,就会溢出
  */
static final int MAXIMUM_CAPACITY = 1 << 30;
 /**
  *默认负载因子,代表了table的填充度为多少。
  *当数组的填充度超过这个负载因子,就要进行扩容
  */
static final float DEFAULT_LOAD_FACTOR = 0.75F;
 /**
  *将链表转换为红黑树的阈值
  */
static final int TREEIFY_THRESHOLD = 8; 
 /**
  *将红黑树转换为链表的阈值
  */
static final int UNTREEIFY_THRESHOLD = 6;
 /**
  *最小树化容量,这个值代表要将数组中的链表进行树化(转换为红黑树)的最小数组容量。
  *也就是说,数组的长度要达到64,才能将长度超过8的链表转换为红黑树
  */
static final int MIN_TREEIFY_CAPACITY = 64;
 /**
  *table这个数组就是HashMap中底层用来存储节点的数组
  *利用hash散列将节点均匀的分配在这个数组中,出现冲突,就使用拉链表法
  */
transient HashMap.Node<K, V>[] table;
transient Set<Entry<K, V>> entrySet; //HashMap中所有节点的键值对集合
transient int size;//记录当前HashMap中元素个数
transient int modCount;//修改次数
int threshold; //当table的容量超过这个值时,要进行扩容
final float loadFactor;//负载因子

HashMap构造函数

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段默认
    }

默认的构造函数仅仅是设定了默认的负载因子。没有对table数组进行初始化,实际上对table数组的初始化是在添加元素的时候通过扩容实现的初始化

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)//初始容量<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);
    }
//这个方法用于找到大于等于initialCapacity的最小的2的幂
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;
    }

从构造函数来看,我们可以设置HashMap的负载因子loadFactor,虽然我们制定了容量,但是这里并没有代表容量的属性,而是我们利用初始化的容量参数 initialCapacitythreshold 进行了初始化且保证threshold是2的次幂。

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

在第一个构造函数的基础上使用了默认的负载因子

HashMap方法分析

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

HashMap是允许键为空值的,如果利用null直接调用Object的hashCode方法,则会抛出空指针异常,而这里则对空值进行了一个判断,当为空值时,返回的hash值为0。其次,为了充分的散列,降低碰撞率,将key的高16位与低16位进行异或

put()
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;
        //判断table是否为空或者是否为0,若是,进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//扩容
        //判断通过hash计算出的索引出是否为空节点,若为空节点,直接将新节点放到该位置    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {    //如果该节点不为空,说明发生冲突,或者已经存在该节点
            Node<K,V> e; K k;
           //如果该节点和要插入的节点相同,记录该节点
            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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) //判断是否达到树化的标准
                            treeifyBin(tab, hash);
                        break;
                    }
                    //找到相同节点,break;
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // 如果e不为空,说明已经存在该节点,记录旧值,更新value
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;  //返回旧值
            }
        }
        ++modCount;
        if (++size > threshold) //加入节点后,判断是否达到扩容的阈值
            resize();
        afterNodeInsertion(evict);
        return null; //正确插入返回null
    }
  1. 判断table是否为空,若为空,则进行扩容
  2. 判断通过key的hash值计算出的索引处节点是否为空,若为空,将新节点插入该处
  3. 如果索引处的节点不为空,判断first的hash值是否与key的hash值是否相同,若不为空,先判断这两个节点是否指向同一个引用,否则用equals方法判断是否相同。相同则记录该节点
  4. 如果不同,则判断该节点是否为TreeNode,若为TreeNode,则调用putTreeVal方法
  5. 否则,该节点为链表节点,遍历这个链表
  6. 找到记录该节点e,找不到则将节点加入到链表的尾部
  7. 判断e是否为空,若不为空,说明已经存在该节点,覆盖旧值,返回旧值
  8. 若为空,则表明已经插入新节点。判断节点数量是否达到阈值,若超过阈值则进行扩容
  9. 返回null
resize()
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;  //记录旧的table
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//记录原来table的容量
        int oldThr = threshold;//记录原来的阈值
        int newCap, newThr = 0;
        if (oldCap > 0) {
        	//当原来容量超过最大值时,将阈值设置为Integer.MAX_VALUE,不再进行扩容
            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) // 如果设置了初始容量,则新容量设置为旧的阈值(这个阈值是2的次幂)
            newCap = oldThr;
        else {               
        // 否则如果为0的话,说明使用的是默认的值,容量设为默认容量,阈值为默认的容量与默认的负载因子之积
            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];  //用newCap进行扩容
        table = newTab;				//table更新为newTab
        if (oldTab != null) {		//原来table不为空,则将原来table的内容转移到新的table
            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)
                        ((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;
                            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;
    }

扩容分为四种情况:

  1. 原容量大于等于MAXIMUM_CAPACITY,不扩容,threshold变为Integer.MAX_VALUE
  2. 原容量在DEFAULT_INITIAL_CAPACITY和MAXIMUM_CAPACITY之间,进行两倍扩容
  3. 如果设置了默认容量,将新容量值设置为threshold
  4. 如果使用了默认值,容量扩容为DEFAULT_INITIAL_CAPACITY大小
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; //tab指向原table数组
        Node<K,V> first, e; //first为通过hash函数找到的第一个节点,e为临时变量
        int n; //n为table数组的长度
        K k;  //k记录键值对的key值
        //首先检查table是否为空(未初始化),table长度是否为0,
        //通过hash计算出的下标是否为空,任何一个不满足,则表名查询节点不在该数组中
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            //在数组不为空并且根据hash计算的下标指向的节点也不为空,
            //那么,判断该节点是够为目标节点
            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;
    }

  1. 对table进行验证,table是否为空,长度是否为0,通过key的hash值计算的索引处节点是否为空
  2. 如果以上都满足,判断first节点的hash值是否与key的一致,若一致,则判断first节点是否与key是同一个引用,或者用equals方法判断两个节点是否一致,若一致则返回
  3. 否则判断是否有相邻节点,如果有相邻节点,判断该节点是否是TreeNode,若为TreeNode,则调用getTreeNode方法查找
  4. 否则为链表节点,通过遍历方式查找,找到返回value
  5. 找不到,返回null
remove()
/**
* 从HashMap中删除掉指定Key对应的键值对,并返回被删除的键值对的值
* 如果返回空,说明Key可能不存在,也可能Key对应的值就是null
* 如果想确定到底Key是否存在可以使用containsKey方法
*/
   public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
/**
* 方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的处理逻辑(解析中有描述)
*
* @param hash key的hash值,该值是通过hash(key)获取到的
* @param key 要删除的键值对的key
* @param value 要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true
* @param matchValue 如果为true,则当key对应的键值对的值equals(value)为true时才删除;否则不关心value的值
* @param movable 删除后是否移动节点,如果为false,则不移动
* @return 返回被删除的节点对象,如果没有删除任何节点则返回null
*/
 	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;//声明节点数组、当前节点、数组长度、索引值
     /*
     * 如果节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
     * 需要从该节点p向下遍历,找到那个和key匹配的节点对象
     */
        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;// 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量
           	// 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            /*
	         * 首节点没有匹配上,那么检查下是否有next节点 
	         * 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
	         * 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
	         */
            else if ((e = p.next) != null) {
            	// 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                // 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可 
                else {
                    do {
                     	// 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
					 // e没有匹配上
                        p = e;// 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
                    } while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
                }
            }
	       	/*
	         * 如果node不为空,说明根据key匹配到了要删除的节点
	         * 如果不需要对比value值  或者  需要对比value值但是value值也相等
	         * 那么就可以删除该node节点了
	         */
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)// 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
                    tab[index] = node.next;//由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
                else// 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
                    p.next = node.next;
                ++modCount;// HashMap的修改次数递增
                --size;// HashMap的元素个数递减
			    afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
                return node;
            }
        }
        return null;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值