HaspMap底层核心知识总结

HashMap底层核心知识总结

本文结合底层对HashMap核心知识进行归纳总结!!!

一、了解数据结构中的HashMap吗?介绍下他的结构和底层原理?

  • HashMap是由数组+链表组成的数据结构(jdk1.8中是数组+链表+红⿊树的数据结构)
    HaspMap底层核心知识总结_第1张图片
  • 1.7 版本:根据hash(key)确定存储位置后,以链表的形式在该位置处存数据。此时数组该位置的链表存了多个数据,因此也称为桶存放的数据是⽤Entry描述。
    HaspMap底层核心知识总结_第2张图片
  • 1.8 版本:
    存放的数据是⽤Node描述
    HaspMap底层核心知识总结_第3张图片
    链表有可能过⻓,所以在满⾜以下条件时,链表会转换成红⿊树:
    链表⻓度>8
    数组⼤⼩>=64
  • 1.8版本:当红⿊树节点个数<6时转换为链表

二、HaspMap存储原理

往HashMap添加元素的时候,首先会调用键的hashCode方法得到元素的哈希码值,然后经过运算就可以算出该元素元素在哈希表中的存储位置。

情况1: 如果算出的位置目前没有任何元素存储,那么该元素可以直接添加到哈希表中。

情况2: 如果算出的位置目前已经存在其他的元素,那么还会调用该元素的equals方法与这个位置上的元素进行比较,如果equals方法返回的是false,那么该元素允许被存储,如果equals方法返回的是true,那么该元素被视为重复元素,不允许存储。

三、HashMap怎么设定初始容量大小的?

  • 如果没有指定容量:则使⽤默认的容量为16,负载因⼦0.75。
    HaspMap底层核心知识总结_第4张图片
  • 如果指定了容量,则会初始化容量为:⼤于指定容量的,最近的2的整数次⽅的数。⽐如传⼊是10,则会初始化容量为16(2的4次⽅)。
    HaspMap底层核心知识总结_第5张图片
    该算法的逻辑是让⾼位1的之后所有位上的数都为1,再做+1的操作,实现初始化容量为:⼤于指定容量的,最近的2的整数次⽅的数。

四、HashMap的hash函数是如何设计的?

HaspMap底层核心知识总结_第6张图片
⽤key的hashCode()与其低16位做异或运算。这个扰动函数的设计有两个原因:

  • 计算出来的hash值尽量分散,降级hash碰撞的概率
  • ⽤位运算做算法,更加⾼效

这样答只是答了表象的东⻄,深层的内容是这样的:
⾸先我们要知道hash运算的⽬的是⽤来定位该数据要存放在数组的哪个位置,如何计算?
HaspMap底层核心知识总结_第7张图片
是通过n-1的操作与原hash值做“与”运算,其中n是数组的⻓度。相当于是更⾼效的%取模运
算。⽽n-1恰好是⼀个低位掩码。⽐如初始化⻓度是16,那n-1是15,即⼆进制的00001111。
此时得到了另⼀个问题的答案:那么为什么不能直接⽤key的hashCode()作为hash值,⽽⼀
定要^ (h >>> 16)?
因为如果直接⽤key的hashCode()作为hash值,很容易发⽣hash碰撞。
使⽤扰动函数^ (h >>> 16),就是为了混淆原始哈希码的⾼位和低位,以此来加⼤低位的随机性。且低位中参杂了⾼位的信息,这样⾼位的信息也作为扰动函数的关键信息。
HaspMap底层核心知识总结_第8张图片

五、JDK1.8相比1.7,做了哪些优化?

1.8除了引⼊了红⿊树,将时间复杂度由O(n)降为O(log n)以外,还将1.7的头插法改为1.8的尾插法。
头插法:
作者认为,后插⼊的数据,被访问的概率更⾼,所以使⽤了头插法,但头插法会存在遍历时死循环的情况。
扩容之前:
HaspMap底层核心知识总结_第9张图片
扩容之后:获得新的index,头插法会导致链表反转:
HaspMap底层核心知识总结_第10张图片

/**
 * Transfers all entries from current table to newTable.
 */
 void transfer(Entry[] newTable, boolean rehash) {
	 int newCapacity = newTable.length;
	 for (Entry<K,V> e : table) {
		 while(null != e) {
			 Entry<K,V> next = e.next;
			 if (rehash) {
			 	e.hash = null == e.key ? 0 : hash(e.key);
			 }
			 int i = indexFor(e.hash, newCapacity);
			 e.next = newTable[i]; //此处如果发⽣并发,线程1执⾏反转过程中线程2执⾏
			 newTable[i] = e;
			 e = next;
		}
	}
 }

当线程1执⾏反转过程中线程2执⾏,就可能会出现如下情况,造成链表成环的死循环问题。
HaspMap底层核心知识总结_第11张图片
尾插法:
在扩容时会保持链表元素原先的顺序,因此不会出现链表成环的死循环问题。

六、HashMap怎么实现扩容?

HashMap执⾏扩容关系到两个参数:

  • Capacity:HashMap当前容量
  • loadFactor:负载因⼦(默认是0.75) 当HashMap容量达到Capacity*loadFactor时,进⾏扩容。

1.7和1.8版本的扩容区别:

  • 1.7版本
    先扩容,再插⼊数据。扩容时会创建⼀个为原数组的2倍⼤⼩的数组,然后将原数组的元素重新hash,存进新数组。
  • 1.8版本
    先插⼊数据,再执⾏扩容。扩容时会创建⼀个为原数组的2倍⼤⼩的数组,然后将原数组的元素存进新数组。不同的是1.8使⽤位移操作创建2倍⼤⼩的新数组。

七、插⼊数据时扩容的重新hash是怎么做的?

  • 1.7:需要再做⼀次hash
/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket. It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
 void addEntry(int hash, K key, V value, int bucketIndex) {
	 if ((size >= threshold) && (null != table[bucketIndex])) {
		 resize(2 * table.length);
		 hash = (null != key) ? hash(key) : 0;
		 bucketIndex = indexFor(hash, table.length);
	 }
	 createEntry(hash, key, value, bucketIndex);
 }
  • 1.8:不需要做hash,通过原⽅式获取存储位置
newTab[e.hash & (newCap - 1)] = e;

由于newCap为新数组的⼤⼩,因此在做与操作时,在没有改变key的hash的情况下,改变了与数的值来获取新的存储位置,效率更⾼。⽽且位预算的newCap-1 实际上由于2的幂的关系,-1的操作实际上就是在⾼位补1,效率更⾼。

八、为什么重写equals⽅法后还要重写hashCode⽅法?

因为在put的时候,如果数据已经存在,就需要把⽼的数据return,存⼊新的数据。那如何判断数据已存在呢?是通过先⽐较hash值,如果hash值相同,再⽤equals判断。

Node<K,V> e; K k;
if (p.hash == hash &&
	 ((k = p.key) == key || (key != null && key.equals(k))))
	 e = p;

重写equals和hashCode⽅法的⽬的就是根据对象的属性来进⾏判断对象是否相同,⽽⾮根据对象的内存地址来判断。

public class User {
 
	 private int id;
	 private String name;
	 @Override
	 public boolean equals(Object o) {
		 if (this == o) return true;
		 if (o == null || getClass() != o.getClass()) return false;
	 		User user = (User) o;
	 	return id == user.id && Objects.equals(name, user.name);
	 }
	 @Override
	 public int hashCode() {
	 	return Objects.hash(id, name);
	 }
}

你可能感兴趣的:(java题,java基础知识,java,HashMap)