作者简介:
HashMap的底层原理面试必考题。为什么面试官如此青睐这道题?
HashMap里面涉及了很多知识点,可以比较全面考察面试者的基本功,想要拿到一个好的offer,这是一个迈不过的坎,接下来我用最通俗易懂的语言带着大家揭开HashMap的神秘面纱。
HashMap的节点:
HashMap是一个集合,键值对的集合,源码中每个节点用Node<key,value>表示。
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的数据结构为 数组+(链表或红黑树)如图:
为什么采用这种结构来存储元素呢?
1:数组的特点:查询效率高,插入,删除效率低;
2:链表的特点:查询效率低,插入,删除效率高;
在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值和数组长度取模我们可以得到元素存储的下标。
假设:刘德华的hashcode为20977295数组长度为16 则要存储在数组的索引为 20977295%16=1的地方。
可以分两种情况:
1:数组索引为1的地方是空的,这种情况简单,直接将元素存放进去就好了。
2:已经有元素占据了索引1的位置,这种情况下我们需要判断一下位置元素和当前元素是否相等,使用equals来比较。
如果使用默认规则是比较两个对象地址。也就是两者需要是通一个对象才相等,当然我们也可以重写equals方法来实现我们自己的比较规则最常见的是通过属性值来判断是否相等。
如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素。
每一个元素节点都有一个next属性指向下一个节点,这里由数组结构变成了数组+链表结构,红黑树又是怎么回事?
因为链表中的元素太多的时候会影响查询效率,所以当链表元素的个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因是就是红黑树是平衡二叉树,在查找性能方面比链表要高。
HashMap中的两个重要的参数
初始容量大小和加载因子
初始容量大小是创建时给数组分配的容量大小,默认值16,加载因子为0.75,用数组容量的大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫扩容。
在扩容的时候会产生一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组中,所以扩容的性能非常消耗性能。
注意:
创建HashMap时我们可以通过合理的设置初始容量大小来达到尽量少的扩容的目的。加载因子也可以设置,但是除非特殊情况不建议设置.