什么是HashMap
HashMap是线程不安全的常用的双列集合,里面存的是key-value键值对组成的一个个entry, key和value都可以为null,但最多只有一个key可以为null,jdk8及之后对于Entry数组的初始化延迟到了第一次put操作时 ;存入map中的key-value 其中value 一定要重写equals方法和hashcode方法
数据结构
1.7底层:数组+链表
1.8底层:数组+链表+红黑树
具体来说存的是一个个的Entry,其中每个Entry中又有:Key、Value、Hash值、Next指针
为什么引入红黑树
哈希冲突过多时,链表过长,性能会下降,时间复杂度为On,引入红黑树之后当链表过了阈值8之后会发生树化,性能为Ologn,反树化阈值为6
树化条件
数组长度大于等于64 并且 某条链表长度大于树化阈值8 那么这条链表会变为红黑树
扩容时机
第一种情况:当存入hashmap中的元素超过阈值之后扩容为原先长度的2倍
第二种情况:当存入元素未超过阈值时,并且数组长度不超过64,但某条链表上元素个数超过树化阈值8时会发生扩容为原来的2倍
计算下标方法
根据k-v的k计算下标,首先调用k的hashcode方法得到一个值h 然后将h与h无符号右移16位之后的值进行异或运算 (这样主要是为了让高位也参与,进而减少哈希冲突),最后得到一个二次哈希值,然后将这个二次哈希值与 容量-1 进行与运算
负载因子为什么是0.75
什么是哈希冲突,遇到哈希冲突怎么办
两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用;比如有俩元素都要存入map,根据他们各自的key计算出来的下标相同,也就是他们想存入同一个位置;
解决办法:
开放定址法:具体分为 线性探查法、平方探查法、双散列函数探查法 ,简单说就是当发生哈希冲突时,按一定的规则找到一个空闲的单元,把发生冲突的元素放入。 比如说线性探查法,就是 查看发生冲突的单元的下一个单元是否为空。
拉链法:将哈希值相同的元素,构成一个链表
再次哈希法:当哈希冲突时,再调用另一个哈希函数计算,直到不再冲突。
建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区
数组容量为何是2的次幂
1.为了hash%n == hash&(n-1) //为了 位运算的成立,位运算效率更高
2.假设现在数组的长度 length 可能是偶数也可能是奇数
length 为偶数时,length-1 为奇数,奇数的二进制最后一位是 1,这样便保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。
例如 length = 4,length - 1 = 3, 3 的 二进制是 11
若此时的 hash 是 2,也就是 10,那么 10 & 11 = 10(偶数位置)
hash = 3,即 11 & 11 = 11 (奇数位置)
而如果 length 为奇数的话,很明显 length-1 为偶数,它的最后一位是 0,这样 hash & (length-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间
为什么HashMap线程不安全
Jdk1.7的时候在扩容时候调用resize方法,此时并发容易形成环形链表 有可能获取这个下标处的元素时候造成死循环;而jdk1.8的时候修复了死循环的问题 ,但有可能会出现数值覆盖(俩线程同时发现某个桶没有元素,A线程写了他的值进来,会被B覆盖掉)
HashMap的扩容机制
懒加载,首次put的时候扩容为长度16的数组
首先经过一些判断,计算出新的数组大小和扩容阈值,比如说判断数组大小是否已经达到int的最大值,如果是,就把扩容阈值调整为int的最大值,如果不是就把新容量设置为旧容量的2倍,新阈值设置为旧阈值的2倍。然后开始将旧表中的节点转移到新表中。每个桶依据三种情况: 单个元素、已经形成红黑树、已经形成链表。
单个元素:依据原先的hash值重新与新表长度-1进行&运算寻找新的下标 :要么 旧位置 要么 旧位置+旧容量
红黑树:下次再写..
链表:依次判断每个节点 属于 低位链表还是高位链表,也就是旧位置的 和 旧位置+旧容量的 分别放入对应的桶中
重点就是:
如何寻找新下标 (注意,扩容之后移动元素 是不用重新计算哈希值的 只需计算新下标):
不需要重新计算哈希值,要么 旧位置 要么 旧位置+旧容量
比如说俩元素的二次哈希值分别为 0001 0101 与 0000 0101 ,那么他们与长度16的数组 -1之后 的0000 0101 进行&运算得到的下标都是 0000 0101 也就是下标为5的位置,如果每次数组都扩容二倍,16变为32 n-1就变成了 0001 0101 这俩元素进行&运算之后分别变成了 0001 0101 和 0000 0101 也就是下标分别是 5+16 和 5
如何遍历HashMap
- 可以调用map的entrySet方法获取所有的entry 然后使用增强for循环遍历每个entry的同时调用entry的getKey方法或者getValue方法获取key-value。 (推荐使用, 而且遍历Set集合的适合 可以采用迭代器方法)
- 可以调用map的keySet方法获取所有的key为一个Set单列集合,利用Set的迭代器遍历key调用get方法获取每个key的value
常用的方法
put(k,v) 向map中存入键值对
remove(k) 删除map中键为k的数据
get(k) 根据k取出对应的value
containsKey(k) 判断集合中是否存在键为k的数据
size() 返回map集合中元素数量
isEmpty() 判断map中是否有数据
entrySet()、 keySet() 返回 entry组成的Set集合 或者 返回key组成的Set集合