HashMap是如何实现的(底层原理)?

HashMap底层原理

  • 简述
            HashMap是Java开发过程中使用最多的集合之一,其中的put(key,value)和get(key)方法,是我们在使用HashMap时最常使用的方法,但是底层的原理我们可能就了解得比较少了,或许你知道HashMap是使用hash算法,然后基于数组+链表+红黑树来实现的,或许还知道HashMap内部数组的初始长度为16,并且还能自动扩容.今天我们就来探究一下put(key,value)和get(key)这两个常用的方法的实现过程和原理.

  • put(key,value)的原理
          首先我们调用put方法的时候,方法内部会调用hash(key)方法,计算出key的hash值h,然后通过HashMap的主干部分(数组tab)的长度length来进行位与运算(length-1)&h得出一个index=(length-1)&h值,然后直接根据index的值,将Node对象放入对应的tab[index]=Node即可,如下面伪代码和图所示:

		HashMap mapDemo=new HashMap()
		mapDemo.put("name","Aiden")时,
		Node node=new Node("name","Aiden");
		int index=hash("name")&length-1,假如算出index=2
		tab[2]=node;
TIM截图20180701114213

      虽然在计算index的时候采用的是均匀分布算法,但是当put的key-value越来越多时,不可避免的会出现计算的index出现重复,这个时候就需要用到链表结构了,其实我们的Node对象不仅仅是数组里面的一个元素,还是可能是一个链表的头节点,通过next指向链表的下一个Node对象节点.当出现重复index的时候则以下面的方式进行插入:

		mapDemo.put("name","Aiden")时,
		Node node3=new Node("galary","1K");
		int index=hash("galary")&length-1
		假如算出index=3,发现数组里面里面index=3的位置已经有Node对象node2了
		这个时候就转换成链表结构原来的Node对象成为链表的头节点,
		新put的Node对象,插入依次链表的尾部("尾插法"),如图:
尾插法

另外当链表的长度达到一定值(默认为8)之后,会转换成红黑树的结构,如下图:

红黑树转换
  • get(key)的原理

        一般情况下,首先根据key值调用方法hash(key),通过与数组长度位与运算来计算出index值,然后直接返回tab[index].value即可.
    但是当计算出的index处的Node对象是链表结构或红黑树时,应该怎么办呢,因为那里所有Node的index都是相同的.

    • 当是链表结构时,这个时候会使用node.next来依次遍历比较链表上每个Node对象节点的key值是否和传入的key相等,直到匹配上为止.如下伪代码和图所示:
	mapDemo.get("galary");
	int index=hash("galary")&length-1,假如index=3,
	这个时候我们获取tab[3]发现tab[3].key为"nick",这显然不对.
	那么使用链表next指向下一个元素的特性进行遍历查找
	Node e=tab[3];
	while((e=e.next) != null){
		if("galary".equals(e.key)){//当遍历到e=Node7的时候显然要找的就是它
			return e.value;	
		}
	}

get原理



当是红黑树结构时则node实际就为TreeNode对象,由对象调用getTreeNode方法进行内部匹配,这里对红黑树的内部如何匹配就不做介绍了,感兴趣的朋友可以自行查资料.


注:

  • 1. 为什么要进行位与运算来得出index呢,主要是为了实现index的均匀分布.均匀分布的原理如下:
             假如一个key="water",计算water的hashcode,结果二进制的11010111010110001010001 0111。假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。把以上两个结果做与运算,11010111010110001010001 0111 & 1111 = 0111,十进制是7,所以 index=7。另外为什么HashMap长度是默认16或者2的幂,因为当其长度为2的幂时,length-1的二进制位全为1,比如:31的二进制为11111;63的二进制为111111,所以,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的,这就就是为什么是和length-1做位与运算,而不是其他值.
  • 2.  Node键值对用来存放key-value的,Java8之前是是使用的Entry键值对,NodeEntry均实现Map.Entry接口.
  • 3. 尾插法,在链表的尾部依次插入新的Node节点,Java8之前都是使用的头插法,即每一个新的Node都替代原来的头节点,原来的头节点变成了由新的Node节点用next指向.
  • 源码解读
  • put(key,value)方法源码解读
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);	//此处参数内hash(key),实际上是获得key的hashCode值
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {// onlyIfAbsent值为false的时候表示直接覆盖已存在的key的value值,为true时oldValue为null则才会覆盖
    Node<K, V>[] tab;					// 声明一个tab数组
    Node<K, V> p;						// 声明一个节点Node键值对
    int n, i;						// n表示tab的size; 表示即将把值put到某个Node(key-value)的index
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;	                // 如果tab为空或者长度为0时,就初始化tab(初始长度为16),并将长度值赋给n
    if ((p = tab[i = (n - 1) & hash]) == null)	        // 当(n - 1) & hash 位与运算的结果为key对应的index,当为控的时候
        tab[i] = newNode(hash, key, value, null);	// 则调用newNode方法新建一个Node对象赋值给tab[i]
    else {							//  进入此else的代码块,则表示tab对应index必定已存在Node对象
        Node<K, V> e;
	K k;						// 泛型 对象k  代表key
    if (p.hash == hash&& ((k = p.key) == key || (key != null && key.equals(k))))// 当对应index上的Node的hash值与传入进来的key的hash值相等并且Node的key和传入的key也相等时则覆盖value值
        e = p;
    else if (p instanceof TreeNode)			// 当上面不成立的时候,则说明此Node可能是红黑树或者链表. 当 是红黑树的时候
        e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 当为红黑树时,调用putTreeVal方法,此方法内部会遍历红黑树,如果已存在则返回被覆盖的Node,否则返回null
    else {						 // 当为链表时
        for (int binCount = 0;; ++binCount) {	        // 循环遍历链表
	    if ((e = p.next) == null) {	               // 当p.next为空时  
	        p.next = newNode(hash, key, value, null);// 则newNode方法新建一个Node对象赋值给p.next
	        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 当链表达到一定长度(默认8)后,调用treeifyBin方法转换成红黑树的结构
		    treeifyBin(tab, hash); // 当遍历到p.next为空时 则表示找到了可以放置put进来的key-value的地方,所以要结束链表遍历
	    }
	    if (e.hash == hash&& ((k = e.key) == key || (key != null && key.equals(k)))) // 当p.next不为空时,并且hash值和传入的hash值相等,key值也相等时,则覆盖值,结束循环
	        break;	                          // 则表示覆盖值,结束循环
		p = e;				 //因为链表的遍历是 p.next.next.next...这种形式,所以最后需要将e赋给p
	 }
     }
     if (e != null) { // existing mapping for key   //此处是真正执行覆盖操作的地方
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)  //onlyIfAbsent传入为false,所以会直接覆盖e原来的value值
	    e.value = value;
	afterNodeAccess(e);			//afterNodeAccess此方法下面没有代码所以无实际意义
	return oldValue;			// 这个地方可以看出 当我们put重复的key-value时  会返回被覆盖的oldValue,虽然我们一般不关心put时候的返回值
      }
    }
    ++modCount;					//modCount是统计map被修改次数的
     if (++size > threshold)			//当tab的size大于阈值的时候则需要调用resize方法进行扩容
         resize();afterNodeInsertion(evict);	//afterNodeInsertion此方法下面没有代码所以无实际意义
     return null;				//当put的时候不存在覆盖值的时候则返回null
}
  • get(key)源码解读
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;
		Node<K, V> first, e;
		int n;
		K k;
		if ((tab = table) != null && (n = tab.length) > 0 // 当tab不为空并且长度不为0的情况下
				&& (first = tab[(n - 1) & hash]) != null) {//    并且通过hash值和长度-1的位运算计算出的table[index]不为空
			if (first.hash == hash &&                           // check匹配上的index处的节点,比较key值是否相等
					((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);//红黑树TreeNode内部匹配结果
				do {
					if (e.hash == hash&& ((k = e.key) == key || (key != null && key.equals(k))))
					            	            //当为链表的时候则使用next指针遍历整个链表直到匹配上或者遍历完
						return e;
				} while ((e = e.next) != null);//当下一个节点不为空时,继续比较key值是否匹配(执行do{}代码块)
			}
		}
		return null;
	}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值