Java基础---HashMap源码学习

HashMap源码学习(一)

开头想先说一点题外话 在以前初学HashMap的时候 就认识到了要对源码进行学习 可当点开讲解源码的博客 发现这是一件太困难的事情 一直以来 对于HashMap也没有深入的理解其底层原理 今天趁着这个机会希望加深自己对于集合的认识和理解

源码学习确实是一个让人头疼的问题 在学习过程中 参考了许多优秀的博客 这里要说 我的这篇文章引用了美团技术团队这篇文章中的诸多文字 图片 内容 会进行很多的引用 美团技术团队这篇文章是我目前觉得讲解HashMap最清楚的一片文章 另外 我的这篇文章的后续加入了自己的一点理解和补充 如果有理解偏差的地方 希望大家指正 下面开始正题

什么是HashMap

HashMap是数组+链表+红黑树组成的 用来存放Key-Value的键值对集合 每一个键值对也叫Node

这些Node分散在一个Node<K,V>[] tab数组中(存储位置由Hash函数决定)

数组的长度是有限的 当元素发生hash冲突之后 就会响应的转换为链表或者红黑树

存储结构

在这里插入图片描述

从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。我们来看Node[JDK1.8]是何物

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用来定位数组索引位置
        final K key;
        V value;
        Node<K,V> next;   //链表的下一个node

        Node(int hash, K key, V value, Node<K,V> next) { ... }
        public final K getKey(){ ... }
        public final V getValue() { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
}

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象

Hash函数

Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。

这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

下面举例说明下,n为table的长度

在这里插入图片描述

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

还有在putVal方法中的一段代码

 if ((p = tab[i = (n - 1) & hash]) == null)

上述两段代码印证了以上过程

HashMap的put方法

在这里插入图片描述

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容

  public V put(K key, V value) {
      // 对key的hashCode()做hash
      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;
      // 步骤①:tab为空则创建
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     // 步骤②:计算index,并对null做处理 
     if ((p = tab[i = (n - 1) & hash]) == null) 
         tab[i] = newNode(hash, key, value, null);
     else {
         Node<K,V> e; K k;
         // 步骤③:节点key存在,直接覆盖value
         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);
                        //链表长度大于8转换为红黑树进行处理
                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                         treeifyBin(tab, hash);
                     break;
                 }
                    // key已经存在直接覆盖value
                 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;
             if (!onlyIfAbsent || oldValue == null)
                 e.value = value;
             afterNodeAccess(e);
             return oldValue;
         }
     }

     ++modCount;
     // 步骤⑥:超过最大容量 就扩容
     if (++size > threshold)
         resize();
     afterNodeInsertion(evict);
     return null;
 }

自己的测试例子—模拟Hash冲突(1)

这部分内容如有不对 请大家指正

class User{
	int id;
	String name;
	public User(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	@Override
	public int hashCode() {
		return this.id;
	}/*
	@Override
	public boolean equals(Object obj) {
		User u=(User)obj;
		return this.id==u.id;
	}*/
	
}
class Test{
	public static void main(String[] args) {
		User u1=new User(1,"小明");
		User u2=new User(1,"明明");
		HashMap<User,String> hm=new HashMap<User, String>();
		hm.put(u1,"他喜欢打篮球");
		hm.put(u2,"他喜欢游泳");
		System.out.println(hm);
		
	}
}

在这里插入图片描述

在这个测试中 我们重写了User类的hashCode方法 这个方法将id作为返回值 那么u1和u2的put过程中(u1和u2的id一样 那么hashCode一样) 将会进行过Hash算法之后 也就是取key的hashCode值、高位运算、取模运算这三个步骤之后 计算出的元素在数组中的存储位置一样 也就是说他们发生了Hash冲突 上面的代码中 我们将元素的equals方法进行了注释 那么此时u1和u2就处于 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))这个判断之中 由于equals方法进行了注释 不满足这种情况 无法进行value的直接覆盖 那么接下来进行了链表插入 结果是将两个value值都存入到了hm中 我们看到了如上图的结果

自己的测试例子—模拟Hash冲突(2)

现在我们将equals方法打开 看看会出现什么样的情况

class User{
	int id;
	String name;
	public User(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	@Override
	public int hashCode() {
		return this.id;
	}
	@Override
	public boolean equals(Object obj) {
		User u=(User)obj;
		return this.id==u.id;
	}
	
}
class Test{
	public static void main(String[] args) {
		User u1=new User(1,"小明");
		User u2=new User(1,"明明");
		HashMap<User,String> hm=new HashMap<User, String>();
		hm.put(u1,"他喜欢打篮球");
		hm.put(u2,"他喜欢游泳");
		System.out.println(hm);
		
	}
}

在这里插入图片描述

结果和我们想的一致 将equals方法打开后 在同样发生了Hash冲突的情况下 由于Key一致 所以直接进行了Value的覆盖 在结果中我们只看到后一个value

发布了39 篇原创文章 · 获赞 19 · 访问量 1489

猜你喜欢

转载自blog.csdn.net/weixin_44222272/article/details/104639686