本文转载于知乎:https://zhuanlan.zhihu.com/p/79507868
一:HashMap的节点:
HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next;
Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。
二:HashMap的数据结构
HashMap的数据结构为 数组+(链表或红黑树),
为什么采用这种结构来存储元素呢?
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
三:HashMap存储元素的过程
有这样一段代码:
HashMap<String,String> map = new HashMap<String,String>(); map.put("刘德华","张惠妹"); map.put("张学友","大S");
现在我要把键值对 “刘德华”,”张惠妹”存入map:
第一步:计算出键“刘德华”的hashcode,该值用来定位要将这个元素存放到数组中的什么位置;什么是hashcode?
在Object类中有一个方法:
public native int hashCode();
该方法用native修饰,所以是一个本地方法,所谓本地方法就是非java代码,这个代码通常用c或c++写成,在java中可以去调用它。调用这个方法会生成一个int型的整数,我们叫它哈希码,哈希码和调用它的对象地址和内容有关.
哈希码的特点是:对于同一个对象如果没有被修改(使用equals比较返回true)那么无论何时它的hashcode值都是相同的对于两个对象如果他们的equals返回false,那么他们的hashcode值也有可能相等
明白了hashcode我们再来看元素如何通过hashcode定位到要存储在数组的哪里,通过hashcode值和数组长度取模我们可以得到元素存储的下标。jdk1.6之后改成 index = hash & ( Entry[].length - 1),因为取模运算比位运算慢。
刘德华的hashcode为20977295 数组长度为 16则要存储在数组索引为 20977295%16=1的地方
可以分两种情况: 1. 数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。 2. 已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。 如果使用默认的规则是比较两个对象的地址。也就是两者需要是同一个对象才相等,当然我们也可以重写equals方法来实现我们自己的比较规则最常见的是通过比较属性值来判断是否相等。 如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素
每个元素节点都有一个next属性指向下一个节点,这里由数组结构变成了数组+链表结构,红黑树又是怎么回事呢?因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高.
Hash冲突:
HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存
储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该
Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。当链表节点大于8时,链表转换成红黑树。
HashMap中的两个重要的参数:
HashMap中有两个重要的参数:初始容量大小和加载因子,初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用resize方法将数组容量增加到原来的两倍,专业术语叫做扩容.在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能.
负载因子: 当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会 增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存 空间。
-
如果没有特别需求,不要轻易进行更改,因为 JDK 自身的默认负载因子是非常符合通用场景的需求的。
-
如果确实需要调整,建议不要设置超过 0.75 的数值,因为会显著增加冲突,降低 HashMap 的性能。
- 如果使用太小的负载因子,按照上面的公式,预设容量值也进行调整,否则可能会导致更加频繁的扩容,增加无谓的开销,本身访问性能也会受影响。