一、前言
看了下上一篇博客已经是半个月前,将近20天前了,很惭愧没有坚持下来,这期间主要是受奥运会和王宝强事件的影响,另外加上HashMap中关于rehash的实现比较不好理解,所以就一拖再拖。如果能坚持,也许容器这一部分已经写完了。
不管怎么样,奥运总算是结束了,到年底来说,也没有太多可以关注的内容了,坚持下来,做总比不做好。
回到正题,本文要分析的是Map中的经典实现,HashMap. 顾名思义是通过Hash来查找和存储元素的Map, 这种Map定位方便,支持自动扩容,是一种效率比较高的非线程安全的Map。下面做详细的介绍。
二、存储介绍
HashMap的存储相对于之前我们介绍的List要复杂一些,它使用了数组和链表相结合的方式,一个示例结构如下:
上图大致描述了HashMap的存储结构,即其内部首先是通过对key进行hash, 根据其hash值映射到内部的数组中的某一个位置,但考虑到hash冲突的可能性,所以对于相同hash值而key不同的元素,再通过一个链表进行存储。
也就是说,实际上这个数组中存储的是链表的头元素,每个节点都是一个Entry类型的对象,其包括了hash值,key, value及指向下一个节点的指针。
三、实现分析
根据上一节的介绍,对于map来说,需要实现一些常用而重要的功能,如put,get,delete,search等,本节就来分析一下对于这些功能, hashMap是怎么实现的。
3.1 添加节点, put.
根据map的特点,其key是惟一的,而且允许key为null, 所以,在向map中添加一个元素,需要考虑这样几个问题。
1)key==null的情况下,其hash值如何计算?
如果key==null,这种情况下,直接将key映射到下标为0的位置
2)key已经存在的情况下,如何处理?
将key的value设置为当前值,原来的值直接返回。
3)以什么样的机制扩容?
HashMap内部有一个叫threshold的值,这个值被称之为阈值,初始时,是容量*负载因子,如果当前的map中的元素个数达到这个阈值,且当前映射的位置已经有元素的话,则会对内部的table进行扩容,将其扩大一倍,并对原来的元素重新映射。
这样做是很有必要的,否则当map中的元素过多的话,就容易造成大量的key映射之后的值是同一个下标,这样就影响了hash的存取效率。
4)如何查找key是否存在
如果key为null的话,直接定位到数组中下标为0的位置,否则要先计算hash值,计算完了之后再根据hash值映射到对应的下标。
找到下标后,要看该下标中如果没有链表,则创建一个,如果有的话,则需要依次遍历链表,比较节点的key是否和当前的key相同,比较的逻辑如下:
int hash = hash(key); int i = indexFor(hash, table.length);//table中的位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //entry相同的条件 , hash相同 , key的引用相同,或者equals() if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
可以看到,首先要保证hash值一样,再判断key是否相同或equals
如果没有找到匹配的确实需要添加的话,HashMap采用了个非常巧妙的方式,将元素添加到链表中的头节点,这样省去了遍历。
3.2 查找
查找的逻辑其实和添加时对key的查找是类似的,没有找到的话,直接返回null.
HashMap中对于containsValue的实现比较简单,直接遍历数组的每个下标,再遍历每个下标中的节点,这是一个双重循环,效率相对比较低下,所以这个方法一般情况下不要使用。
3.3 删除
删除的查找过程和前面类似,找到后的删除逻辑如下:
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
这里主要是对于链表的修改,如果是头节点则指向下一个,否则指向头节点。
3.4 遍历
常见的是获取keySet()以及entrySet(),当然也可以获取值集合values(), 这在内部是通过迭代器来实现的。
如果我们要遍历整个map, 最理想的方式是获取entrySet(),这样直接取其key和value即可,而不是获取keySet。
四、总结
理解了HashMap的存储结构之后,那么对于其各种操作的实现也就不难理解了,hashMap中,如何生成hash值,以及是否需要rehash,这一部分是不太好理解的,个人也没理解得太透彻,所以本文没做介绍,有时间的话再做研究。