什么是HashMap?
要想知道什么是HashMap就要先了解数组和链表。数组:查询速度快,可以根据索引查询;但插入和删除比较困难;链表:查询速度慢,需要遍历整个链表,但插入和删除操作比较容易。HashMap是数组和链表组成的,数据结构中又叫“链表散列”。HashMap是Java中集合的一部分。它提供了Java的Map接口的基本实现。它将数据存储在(Key,Value)对中。要访问一个值,你必须知道它的密钥,否则,你不能访问它。
HashMap的过程
- 通过较为复杂的方法把数据均匀散列到数组中,并在数组中记录链表(或者红黑树)的地址。
- 通过一定的方法把数据有规律的存储在链表(或者红黑树)中。
HashMap的过程会出现的问题
(这里需要补充一下——数据倾斜:数组中的一个数组链表不断插入数据,超过他的最大值)
- 如果出现数据倾斜,有可能是因为选择的散列方法不合适,需要更改合适的散列方法。当数组散列出现数据倾斜时(都去一个地方了)会发生红黑树深度特别大的时候需要改变散列算法。(这不是重哈希)
-
如果hashmap使用的是链表并且链表的最大值当数据超过最大值时重新申请数据链表重新散列。
-
如果hashmap使用的是红黑树并且当数据总量超过75%时在进行扩容,重新进行数据散列,重新构建红黑树分配数据。
-
既然红黑树那么好,为啥hashmap不直接采用红黑树,而是当大于8个的时候才转换红黑树?如果元素小于8个,查询成本高,新增成本低,如果元素大于8个,查询成本低,新增成本高。
-
hashmap的线程不安全。
-
map一对一的应对关系:不只有一个数字肯定还有对应的一个内存地址。
HashMap的时间复杂度
在理想的情况下hashmap的时间复杂度为O(1),当链表换成红黑树时hashmap查找的时间复杂依然是O(1),插入和读取的时候对链表进行遍历(一般遍历的 时间复杂度为O(n))但在这并不是这样,因为链表长度固定所以hashmap的时间复杂度为常数即O(1)。
HashMap流程图
HashMap结构示意图
HashMap 内部结构:可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶,每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分key、value,next(指向下一个Entry),通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象(键值对),则以链表形式存储。当链表超过8时,换成红黑树结构。
比较hashCode和equals的区别
hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。
HashMap的初始值还要考虑加载因子:(这是用的别人的)
· 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
· 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
· 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
源码:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//把 oldTab 中的节点 reHash 到 newTab 中去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//若节点是单个节点,直接在 newTab 中进行重定位
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//若是链表,进行链表的 rehash 操作
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//根据算法 e.hash & oldCap 判断节点位置 rehash 后是否发生改变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// rehash 后节点新的位置一定为原来基础上加上 oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结:
HashMap是线程不安全的,多线程环境下数据可能会发生错乱,一定要谨慎使用。HashMap的线程不安全远远不是数据脏读这么简单,它还有可能会发生死锁,造成内存飙升100%的问题。