HashMap实现原理总结

前言:
我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构。



1. 数组结构

public static void main(String[] args) {
	// 数组:采用一段连续的存储单元来存储数据。
	// 特点:指定下标0(1)  删除插入0(N)   查询快 插入慢
	Integer[] integers = new Integer[10];
	integers[0] = 0;
	integers[1] = 1;
	integers[2] = 2;
	
	System.out.println(integers[4]);
}


数组结构: 存储区间连续、内存占用严重、空间复杂度大

优点:随机读取和修改效率高,原因是数组是连续的(随机访问性强,查找速度快)
缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。


2. 链表结构

public class Node {
	private Node next;
	private Object data;
	
	public Node(Object data) {
		this.data = data;
	}
	
	// 链表:一种物理存储单元上非连续,非顺序的存储结构
	// 特点:插入,删除时间复杂度0(1)   查找遍历时间复杂度0(N)   插入快    查找慢
	public static void main(String[] args) {
		Node node = new Node("张三");
		node.next = new Node("李四");
		
		System.out.println(node.data);
		System.out.println(node.next.data);
	}
}

链表结构:存储区间离散、占用内存宽松、空间复杂度小

优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活
缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)


3. HashMap的底层实现原理

HashMap的默认初始容量:16 必须是2的幂次方
HashMap的默认加载因子:0.75


  当我们创建一个HashMap时,默认为其指定了容量大小。大小为16。可以理解成16个 “桶”。每个桶存的就是 K-V 的数据。
  
  在put(key,value)元素时,会根据key的值得到他的hashCode值【hashCode是一个int类型 -2147483648——2147483647 】。在得到hashCode值后,怎么确定把这个元素放到哪个桶呢?
      HashMap采用了一种算法,   index = (n - 1) & hash     n:表示初始容量     hash:该元素key的hashCode值  index:表示得出的该元素要存放到哪个桶的位置。

          为什么初始容量必须是 2的幂次方呢?
              下面我们以值为“book”的Key来演示整个过程:
              1.计算book的hashcode(ps: "book".hashCode() ),结果为十进制的3029737,二进制的101110001110101110 1001。
              2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
              3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
              可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。
              总之一句话,反正初始容量必须是2的幂次方,人家这个算法才能生效。

          那么,既然是2^n ,为啥一定要是16呢?为什么不能是4、8或者32呢?
              关于这个默认容量的选择,JDK并没有给出官方解释。  
              这应该就是个经验值(Experience Value),既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。
              太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。
              所以,16就作为一个经验值被采用了。


          初始容量是不是不能自动变呢?
              初始容量是会自动变的,如果16个 “桶” 都占满了,一直在这16个桶里面操作,难免会影响一定的效率。
              初始容量会在桶被占用了  初始容量*默认加载因子=12  个桶数时进行扩容,扩容一倍的大小。
              
              Hashmap的扩容需要满足两个条件:当前数据存储的数量(即size())大小必须大于等于阈值;当前加入的数据是否发生了hash冲突。 //============来源于网上的说法,不确定。=============//
              因为上面这两个条件,所以存在下面这些情况
                (1)、就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
                (2)、当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。


          在确认了存在哪个位置之后,我们就可以把该元素存放到相应的桶里面。如果存在某2个或多个元素的key经过算法后,他们要存放到桶的位置相同怎么处理呢?
              当出现这种情况,我们称为Hash碰撞。
              在JDK1.8之前 ========链表来解决的=========
              在JDK1.8之后 ========链表个数达到默认值8时将其改为红黑树=========
              

原文地址:https://www.cnblogs.com/itlihao/p/14948189.html