细说JDK1.8下的HashMap

HashMap作为一种重要的数据结构,无论是在面试还是开发中都能经常碰到,今天就来聊一聊它吧。

环境:JDK1.8

一  HashMap的数据结构

HashMap是由数组+链表+红黑树组合而成的,具体来说,HashMap底层维护了一个Entry<K,V>[]的数组,数组中存放了Entry,Entry的结构可能是链表,也能是红黑树。具体下面会分析。

二  HashMap的成员变量

    //序列号
    private static final long serialVersionUID = 362498820763181265L;
    //默认的初始化容量
	static final int DEFAULT_INITIAL_CAPACITY = 16;
    //最大容量
	static final int MAXIMUM_CAPACITY = 1073741824;
    //默认加载因子
	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;
    //存储元素的数组,大小总是2的幂,其中Node实现了Map.Entry接口
	transient Node<K, V>[] table;
    //存放具体元素的集
	transient Set<Map.Entry<K, V>> entrySet;
    //存放元素的个数,注意其与数组长度是不同的
	transient int size;
    //每次扩容和更改map结构时都会用到,用于计数,快速失败会用到
	transient int modCount;
    //临界值,当(容量*加载因子)大于该值时,数组会进行扩容
	int threshold;
    //加载因子
	final float loadFactor;

其中TREEIFY_THRESHOLD,UNTREEIFY_THRESHOLD,MIN_TREEIFY_CAPACITY这三个常量决定了数组中链表和红黑树之间相互转换的关系,数组中一开始存放的都是链表,只有当链表的长度大于8,并且数组的大小大于64时,链表就会转变为红黑树,而当红黑树的大小小于6时,红黑树就会转变为链表。

三  get方法原理解析

	//get方法其实底层是调用了getNode方法,入参为key的hash值和key对象
    public V get(Object paramObject) {
		Node localNode;
		return (localNode = getNode(hash(paramObject), paramObject)) == null ? null : value;
	}
    
    //getNode方法返回的是Node对象,Node实现了Entry
	final Node<K, V> getNode(int paramInt, Object paramObject) {
        //用于存储HashMap的数组
		Node[] arrayOfNode;
        //用于存储数组的长度
		int i;
        //Node实现了Entry实体,用于存储返回值
		Node localNode1;
        //判断数组是否不为空
		if (((arrayOfNode = table) != null) && ((i = arrayOfNode.length) > 0)
				&& ((localNode1 = arrayOfNode[(i - 1 & paramInt)]) != null)) {
			Object localObject;
            //数组中对应角标的元素为所找,直接返回
			if ((hash == paramInt) && (((localObject = key) == paramObject)
					|| ((paramObject != null) && (paramObject.equals(localObject))))) {
				return localNode1;
			}
			Node localNode2;
            //如果数组对应角标对应的第一个元素不为所找元素,寻找该元素的next
			if ((localNode2 = next) != null) {
                //当数组中的结构为红黑树时
				if ((localNode1 instanceof TreeNode)) {
					return ((TreeNode) localNode1).getTreeNode(paramInt, paramObject);
				}
                //当数组中的结构为链表时
				do {
					if ((hash == paramInt) && (((localObject = key) == paramObject)
							|| ((paramObject != null) && (paramObject.equals(localObject))))) {
						return localNode2;
					}
				} while ((localNode2 = next) != null);
			}
		}
		return null;
	}

说明:get方法其实底层就是调用了getNode方法,根据key的值通过(i - 1 & hash)算出其在数组中的角标,然后将角标对应位置的元素的key与传入的key进行比较,如果相等,则返回该元素,如果不相等,则根据该角标对应的结构是红黑树还是链表进行相应的查找。

四  put方法原理解析

//put方法底层是调用了putVal方法
public V put(K paramK, V paramV) {
		return (V) putVal(hash(paramK), paramK, paramV, false, true);
	}

	final V putVal(int paramInt, K paramK, V paramV, boolean paramBoolean1, boolean paramBoolean2) {
		Node[] arrayOfNode;
		int i;
        //对table进行判断,如果table为空,进行扩容
		if (((arrayOfNode = table) == null) || ((i = arrayOfNode.length) == 0)) {
			i = (arrayOfNode = resize()).length;
		}
		int j;
		Object localObject1;
        //(j = i - 1 & paramInt)算出元素放在数组中的对应角标,如果角标上为空,则生成新节点放入
		if ((localObject1 = arrayOfNode[(j = i - 1 & paramInt)]) == null) {
			arrayOfNode[j] = newNode(paramInt, paramK, paramV, null);
		} 
        //角标处已经存在节点
        else {
			Object localObject3;
			Object localObject2;
            //新增的元素与第一个节点hash值和key相等,直接覆盖
			if ((hash == paramInt)
					&& (((localObject3 = key) == paramK) || ((paramK != null) && (paramK.equals(localObject3))))) {
				localObject2 = localObject1;
			}
            //如果角标处存放的红黑树节点
            else if ((localObject1 instanceof TreeNode)) {
                //那就插入一个新的树节点
				localObject2 = ((TreeNode) localObject1).putTreeVal(this, arrayOfNode, paramInt, paramK, paramV);
			} 
            //角标处为链表结构
            else {
				for (int k = 0;; k++) {
					if ((localObject2 = next) == null) {
						next = newNode(paramInt, paramK, paramV, null);
						if (k < 7) {
							break;
						}
                        //如果链表的长度大于8,则将链表转换为红黑树
						treeifyBin(arrayOfNode, paramInt);
						break;
					}
                    //如果新增的元素与链表中的某个节点的hash值和key相等,则进行覆盖
					if ((hash == paramInt) && (((localObject3 = key) == paramK)
							|| ((paramK != null) && (paramK.equals(localObject3))))) {
						break;
					}
					localObject1 = localObject2;
				}
			}
            //当出现key相等的情况,均到这里进行value值的更新处理(即上文说到的覆盖)
			if (localObject2 != null) {
                //将老的value赋值,准备返回
				Object localObject4 = value;
				if ((!paramBoolean1) || (localObject4 == null)) {
                    //将新的value赋值
					value = paramV;
				}
				afterNodeAccess((Node) localObject2);
				return (V) localObject4;
			}
		}
        //修改次数加一,该值与快速失败有关
		modCount += 1;
        //如果hashmap的大小大于阈值,会进行扩容,具体resize函数下文会讲
		if (++size > threshold) {
			resize();
		}
		afterNodeInsertion(paramBoolean2);
		return null;
	}

说明:put方法底层调用了PutVal方法,PutVal先通过(i - 1 & hash)算出要插入元素对应角标的位置,如果对应位置为空,则直接插入节点,如果不为空,即发生了哈希冲突,则需根据节点具体属于链表还是红黑树结构进行插入。

说明

猜你喜欢

转载自blog.csdn.net/Rekeless/article/details/81334658