文章目录
一、概述
我们先来看一张图,回顾一下之前学习的ArrayList、LinkedList、HashMap
(1)数组的优势/劣势
采用一段连续的存储单元来存储数据。它是由相同类型的元素的集合所组成,并且被分配一块连续的内存来存储(与链表对比),利用元素的索引可以计算出该元素对应的存储地址。它的特点是提供随机访问并且容量有限。
缺点:java中内存申请之后大小是固定的,如果数组满了还想再插入数据,就需要重新再建一个容量更大数组,把数据导进去之后再插入新的数据,这个过程很浪费时间。
优点:查找速度很快,对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
(2)链表的优势/劣势
链表是一种线性表,不像顺序表那样连续存储元素,而是在每一个节点里存到下一个节点的指针。
优点:由于不用连续存储,对于链表的新增、删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),比顺序表快得多;
缺点:但查找操作需要遍历链表逐一进行比对,复杂度为O(n),而顺序表相应的时间复杂度分别是O(logn)和O(1)。
(3)散列表的特点
散列表,又叫哈希表(Hash Table),是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。
(4)什么是Hash
哈希:Hash
基本原理就是把任意长度输入,转化为固定长度输出
这个映射的规则就是Hash算法,而原始数据映射的二进制串就是Hash值
Hash的特点
- 1.从Hash值不可以反向推导出原始数据
- 2.输入数据的微小变化会得到完全不同的Hash值,相同的数据一定可以得到相同的值
- 3.哈希算法的执行效率要高效,长的文本也能快速计算Hash值
- 4.Hash算法的冲突概率要小
由于Hash原理就是将输入空间映射成Hash空间,而Hash空间远远小于输入空间,根据抽屉原理,一定存在不同输出有相同的映射
抽屉原理
由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。
抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。
二、HashMap原理讲解
(1)的继承体系
HashMap继承了AbstractMap,实现了Cloneable接口、Serializable接口、Map<K,V>接口
(2)Node数据结构分析
Node节点主要有四个变量:
hash:哈希值,用于路由寻址公式计算出对应的索引位置:index = (table.length - 1) & node.hash
<k,v>:键值对
next:指针
static class Node<K,V> implements Map.Entry<K,V>{
final int hash;//K.hash--->f(k.hash)(扰动函数,让hash值更散列)--->hash
final K key;
V value;
Node<K,V> next;
}
(3)底层存储结构介绍
哈希表,整体是一个数组,不过Node数组的每一项都是由一个单链表组成,下标由hash
值与数组长度与运算(路由寻址)构成。在jdk8中,当数组长度达到64(有些文章里也叫数组容量)并且单链表的长度达到8之后,链表会转化成红黑树的结构。
为什么选择8:当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值(树化门槛)。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。
(4)put数据原理分析
这幅图路由寻址公式index=(table.length - 1) & node.hash
你可能不太理解,它的意思就是index=(表的长度减一)& (哈希值)
表的长度一定是2的幂方次,本例中是16,先16减去1得到15,把15转为二进制为(0000 00000 1111),然后用这个二进制和哈希值做一下与运算得到0010,转为十进制为2,所以下标为2。
(5)什么是Hash碰撞
哈希碰撞(冲突):要把一个对象插入散列表,由路由寻址公式,计算出来的值有可能是一样的。找到这个对象应该插入散列表的位置时,已经存放了其他的对象。
原因:哈希就是做一个映射,为的是查找快。因为映射毕竟是有一个范围的,这个范围可能会小于你原来的那个范围,所以可能好多个值映射了之后成为一个值,即产生哈希冲突。
(6)什么是链化
因为哈希碰撞,插入一个对象时,就会在链表最后面插入(jdk7 头插,jdk8 尾插)。导致链表越来越长,查询会变慢,最后接近为o(n)。
(7)jdk8为什么引入红黑树
解决链化很长的情况,因为红黑树是一个自平衡的二叉查找树,查找效率比链表高。
(8)HashMap扩容原理
16位的数组桶位bin被装满的时候,离散性就变得很差,bin中链表长度会达到阈值(树化门槛),又会导致查询效率变慢。扩容即用空间换时间的思想,提高查找的性能。
三、部分方法的源码:
0.HashMap几个常量
//定义了数组默认大小是2^4
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//定义了数组最大大小是2^30
static final int MAXIMUM_CAPACITY = 1<<30;
//定义了默认的负载因子大小是0.75
static final int DEFAULT_LOAD_FACTOR = 0.75f;
//定义了树化阈值(有一个链长度大于8,并且链表所有元素之和超过64,那么链表结构就变为红黑树)
static final int TREEIFY_THRESHOLD = 8;
//定义了树化阈值(有一个链长度大于8,并且链表所有元素之和超过64,那么链表结构就变为红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;
1.HashMap核心属性(threshold、loadFactory、size、modCount)
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 这就是我们的HashMap散列表表,长度始终是2的幂。
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
// 当前哈希表中元素个数
transient int size;
// 当前哈希表结构修改次数(增、删时才算结构修改)
transient int modCount;
// 负载因子就是0.75
final float loadFactor;
// 扩容阈值,当你的哈希表中的元素超过阈值时,触发扩容。
// 计算方式:threshold = capacity * loadFactor,也就是说“扩容阈值=(表的大小*0.75)”
int threshold;
}
2.HashMap的构造方法
构造方法做了什么呢?
首先人家不知道该创建一个多大的HashMap,所以构造方法先判断调用者有没有把(表的大小Capacity)和(负载因子loadFactor)传进来,
①如果传进来了就用调用者传过来的值(前提是调用者传进来的值可能不规范,我们做一系列加工处理)计算出扩容阈值+负载因子
②如果调用者只传进来(表的大小Capacity)那么(负载因子loadFactor)就使用默认的值计算出扩容阈值+负载因子
③如果调用者只传来一个Map,那么经过一系列操做计算出扩容阈值+负载因子
④如果是空参构造方法,那么只会计算出负载因子(扩容阈值为空!!!)
//构造方法
public HashMap(int initialCapacity, float loadFactor) {
//做一些校验,capacity必须大于0,最大值也就是MAX_CAP
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//lodaFactor负载因子必须大于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//调用了下面的函数
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
// 此方法核心功能就是求出“大于等于输入长度的2次幂的值”
// 如输出:8,输出为8
// 如输出:9,输出为16
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; // n = n | (n >>> 1);
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY