目录:
总结:
1、HashMap 底层数据结构:数组+单链表,存储的是键值对
2、key 可以为 null,但是只能有一个 key 为 null ,允许多条记录的值为null,key为null的存贮位置在数组0号下标(table[0])找 value 的值
3、HashMap是非线程安全
4、数组是HashMap的主体,链表则主要是为了解决哈希冲突而存在的。
5、如果有链表的话,添加,时间复杂度仍然为O(1)。但是查找操作,就需要遍历链表,然后通过key对象的equals 一一对比。
6、二倍扩容
7、hashmap初始化大小是16
8、Hashmap 处理冲突的方法 :
A.链地址法:
最佳情况下 HashMap 查找时间复杂度 O(1)
最坏情况下 HashMap 查找时间复杂度 O(n)
拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
拉链法的缺点
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
有几种常用的探查序列的方法:
B.开放定址法:
a 线性探测法
定义一个哈希算法,算出 index,如果 index 存放了,就看 index+1 有没 有存放。如果 index+1 存放了,就看 index+2 有没有存放,依次类推。
b 随机探测法
将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
c 二次探测法
index+1^2 index-1^2 index+2^2,index-2^2,…,index+k^2,index-k^2 ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
C.公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。
D.再哈希: 定义的多个计算公式(哈希算法):a 哈希算法 b 哈希算法 c 哈希 算法 d 哈希算法,这种方法不易产生聚集,但是增加了计算时间。
源码分析:
一:先介绍JDK1.7
(1)、属性介绍:
默认数组大小 1<<4 = 16
最大大小限制 1<<30 = 2^30
默认加载因子 0.75f
空Entry数组
size大小
threshold阈值 = 当前数组大小*加载因子
modeCount线程安全:其中保存了一个modCount属性 是为了线程安全。遍历的时候++ 使用的时候比较一下这俩一样不。
Entry(key,value)
(2)、put方法:
1、判断是否为空,如果为空就去inflateTable()初始化
toSize是创建hashmap时候传入的大小参数,roundUpToPowerOf2()方法把传入的参数搞成2的次方的数(15->16 , 28->32)然后存入capacity再new一个此大小的Entry当做table(底层数组),判空初始化结束。
为什么是2的次方??
答:计算下标的时候 hash&length-1 做与运算时保证低位数字的值。
2、key可以为null,key可以为null 但是 只有一个key等于null的时候就是把key放到了数组的第0个位置。
3、hash()方法,对key取hash
4、i= indexfor(hash,table.length) 计算i 插入的下标。
让hash值与length-1做与运算,这样可以保证:
1、数组不会越界,
2、低位数字不会改变(与0&的话 全部成0了,hash值就没有意义了,而2的次方-1 的话 低位全部是1,这样就可以保证hash算出的 低位值不变)例:
16:0001 0000
15:0000 1111
%保证index一定是在数组范围内
& cpu计算的时候会更快
table.length 必须是2的幂
5、for(En...)遍历table[ i ]下标的链表,查看是否有相同的key 存在的情况如果有相同key的情况,那么就更新这个key的value的值,并且把老的value用oldValue返回回来
6、如果没有相同的key那么就调用addEntry(hash,key,value,i)然后return null
6.1、if()扩容2倍扩容
threshold阈值
当size大于阈值和当前下标不为null,(jdk1.8中没有)时resize()扩容
扩容类似数组的扩容,就是先创建一个新的数组,然后把数据复制上去
调用transfer()方法进行数据的转移。
rehash一般都为false,然后对新的容量计算一个下标
单线程情况下:
如此循环,但是 这样的话移动完,数据会逆置过来。
多线程之中,,多次循环之后可能会出现构成了循环链表,而导致“死锁”或者叫死循环。原因是线程一已经改变了之前链表的顺序 然后别的线程再来改动的话,就会出现这个问题。
6.2、createEntry(hash,key,value,bucketIndex)增加头结点在 数组上。
(3)、get方法
先算hash,null返回0号下标,然后再在算出的下标里遍历链表。
数组加链表,每次把新的数据插入数组节点,然后链表相当于后移了。
put(key,value)
int hashcode = hash(key)
int index = hashcode % table.length;
table[index] = new Entry(key, value, table[index]);
二:JDK1.8:加入红黑树
(1)、put方法:
public V put(K key, V value) {
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;
//判断是否为空,如果为空就去初始化。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断 计算出的下标的数组位置上是否为空,是空就 new一个节点存入数组,
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//不是空的话 判断是链表,还是树,或者有重复的key
else {
Node<K,V> e; K k;
//是否有相同的key
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 {
//bincount计数,遍历一遍链表,看是不是大于8了
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//新节点插入链表尾部
//****原因:反正都要遍历一遍,所以还不如直接加在最后
// 防止死锁的发生
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果已经存在了key的节点,就把old节点返回
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;
}
put之前先对key进行hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为了让元素更加的散列,右移是让高位也参与算法,因为加入的红黑树,所以hash算法可以比jdk1.7中的简便一点。
static final int TREEIFY_THRESHOLD = 8;
如果一个链表的元素大于8,,就转红黑树
新节点插入链表尾部
//****原因:反正都要遍历一遍,所以还不如直接加在最后
// 防止死锁的发生
因为:32:0010 0000
16:0001 0000
31:0001 1111
所以: 当31与key与的时候 如果key第五位值为1的话,就相当于是 在之前与16计算的基础上加了16,所以: 扩容后的index = index + oldTable.length;