HashMap简介
- HashMap基于哈希表的Map接口实现,是以key-value存储形式存在.
- 系统会根据hash算法来计算key-value的存储位置,可以通过key快速存取value.
HashMap
使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。key相同
的查找? 因为key本身就是对象, 具有hashcode()和equas()方法, 所以先调用hashcode()
方法, 定位到bucket
(桶), 然后再调用键对象的equals()
方法, 对应的属性.
Entry 数据结构
Entry( int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
HashMap的结构图(1.7) Entry数组
+链表
jdk1.8中废除Entry
jdk1.6中,HashMap中有个内置Entry类,它实现了Map.Entry接口;
jdk1.8中,这个Entry类不见了,变成了Node类,也实现了Map.Entry接口,与jdk1.6中的Entry是等价的。
HashMap的结构图(1.8+) Node数组
+链表+红黑树
当链表长度大于8,Node数组结点转为红黑树
//e是p的下一个节点
if ((e = p.next) == null) {
//插入链表的尾部
p.next = newNode(hash, key, value, null);
//如果插入后链表长度大于8则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
存储原理
根据上面图片, 我们可以看出, 如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
一句话: put(k,v), 调用hashcode() get(k)调用key.hashcode() 如果冲突有多个, 再调用key.equals(key2);
负载因子和扩容
initailCapacity * loadFactor = HashMap容量
当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能
,但会增加 Hash 表所占用的内存空间。
如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。
HashMap的大小很简单,不是实时计算的,而是每次新增加Entry/Node的时候,size就递增。删除的时候就递减。当到达阈值(负载因子*总size)时, 容量翻倍, 并且永远是2^n, 因为hash取模运算太慢, 2^n的容量可以进行位运算
头插尾插?
jdk 1.7 头插
jdk 1.8+ 尾插
因为hashmap在并发resize时会出现的死循环
问题, 并且1.7时候用头插是考虑到了一个所谓的热点数据的点(新插入的数据可能会更早用到),但这其实是个伪命题, 因为JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置(就是因为头插) 所以最后的结果 还是打乱了插入的顺序 所以总的来看支撑1.7使用头插的这点原因也不足以支撑下去了 所以就干脆换成尾插 一举多得