1.hashMap的数据结构
hashMap底层采用数组+链表的形式存放数据(JDK1.8之后,引入了红黑树)。当存一个数据时,首先使用hashCode()方法计算这个数据的hash值,hash值确定这个数据存放在数组的哪一个位置,也就是我们所说的哪一个bucket中。确定存放的bucket之后,如果存入的值不等于当前bucket的值,则以链表的形式存放在链表的末尾,当链表的长度超出了一定长度之后,在jdk1.8之后,如果链表超出了一定长度(默认6),出于对性能的考虑,则以红黑树的格式存储数据。
2.关于hashMap的扩容
hashMap的有个默认的负载因子,当存入数据时,如果当前的负载因子大于0.75,则需要对当前的hashmap进行扩容。扩容是一个很耗费时间的过程,因为需要将每个bucket中的数值取出,进行重新计算,从而放入扩容后的hashMap中。因此,需要在声明hashmap的时候,设定初始容量,在阿里巴巴的规范文旦中给出了对于初始容量的描述:
hashmap底层的计算多以位进行计算,因此将初始长度设置成2的指数最为合理;
3.put方法
put方法内部涉及比较多的逻辑,具体的过程如下图所示:
4.get方法
(1)指定key 通过hash函数得到key的hash值
int hash=key.hashCode();
(2)调用内部方法 getNode(),得到桶号(一般为hash值对桶数求模)
int index =hash%Entry[].length;
jdk1.6版本后使用位运算替代模运算,int index=hash&( Entry[].length - 1);
(3)比较桶的内部元素是否与key相等,若都不相等,则没有找到。相等,则取出相等记录的value。
(4)如果得到 key 所在的桶的头结点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。getTreeNode 方法使通过调用树形节点的 find()方法进行查找。由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。
(5)如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;不相等就从子树中递归查找。
HashMap中直接地址用hash函数生成;解决冲突,用比较函数解决。如果每个桶内部只有一个元素,那么查找的时候只有一次比较。当许多桶内没有值时,许多查询就会更快了(指查不到的时候)。
5.解决hash冲突的常见方法
(1). 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。
(2)再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。
(3)建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。
(4) 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
HashMap 就是使用链地址法来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。数组中的每一个单元都会指向一个链表,如果发生冲突,就将 put 进来的 K- V 插入到链表的尾部。
6.总结
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。