日常工作中经常用到MAP,基本上是用Map map=new HashMap()来得到一个HashMap对象,之前并未深入去研究HashMap的实现原理,只是去简单的去创建然后使用它。
这次想深入了解便,去研究了一下HashMap的源码。
做点笔记,记录一下自己的一些收获,想到哪写到哪吧。
HashMap继承自AbstractMap类并实现了Map、Cloneable、Serializable等接口。
HashMap是一种链表散列的数据结构。
链表:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
散列表(Hash表):根据关键码(key-value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
new一个HashMap对象时创建了一个Entry[] table数组, Entry是HashMap的内部类。Entry[] table默认初始化长度为16。
Entry[] table中的某一个元素及其对应的Entry<Key,Value>又被称为桶(bucket);
其结构如下图所示:
以上是HashMap的内部组织结构图。
HashMap的算法实现解析
我们主要解释一下HashMap的put方法:
/** * 将<Key,Value>键值对存到HashMap中,如果Key在HashMap中已经存在,那么最终返回被替换掉的Value值。 * Key 和Value允许为空 */ public V put(K key, V value) { //1.如果key为null,那么将此value放置到table[0],即第一个桶中 if (key == null) return putForNullKey(value); //2.重新计算hashcode值, int hash = hash(key.hashCode()); //3.计算当前hashcode值应当被分配到哪一个桶中,获取桶的索引 int i = indexFor(hash, table.length); //4.循环遍历该桶中的Entry列表 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //5. 查找Entry<Key,Value>链表中是否已经有了以Key值为Key存储的Entry<Key,Value>对象, //已经存在,则将Value值覆盖到对应的Entry<Key,Value>对象节点上 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//请读者注意这个判定条件,非常重要!!! V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //6不存在,则根据键值对<Key,Value> 创建一个新的Entry<Key,Value>对象,然后添加到这个桶的Entry<Key,Value>链表的头部。 addEntry(hash, key, value, i); return null; } /** * Key 为null,则将Entry<null,Value>放置到第一桶table[0]中 */ private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
/** * 根据特定的hashcode 重新计算hash值, * 由于JVM生成的的hashcode的低字节(lower bits)冲突概率大,(JDK只是这么一说,至于为什么我也不清楚) * 为了提高性能,HashMap对Key的hashcode再加工,取Key的hashcode的高字节参与运算 */ static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } /** * 返回此hashcode应当分配到的桶的索引 */ static int indexFor(int h, int length) { return h & (length-1); }
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } /** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
(1)HashMap允许key为空值,当key为空值时,会把key-value键值对放到table[0];
(2)然后计算出Key的hash值并得到它的精确索引地址;
(3)根据索引值定位到table中桶(bucket)的位置,遍历索引处的Entry链表,若hash值相同且key相同,则用新的Value替换老的Value,并返回oldValue;
(4)若不存在相同key,则调用addEntry()方法在索引处新增Entry对象,并放置在当前位置Entry链表的头部。然后判断size是否超过了阀值。若超过阀值则调用resize(int capacity)方法。
(5)HashMap的put方法里会调用一个addEntry的方法,这可能会导致数组长度超过阀值(table数组长度*加载因子),加载因子的经验值为0.75,为了节省空间我们可以增大加载因子,但时间复杂度会增大。
(6)HashMap的默认初始长度为2的4次方,且每次扩充都是在原有的基础上乘2,之前一直对为什么长度要是2的次方迷惑,经过查阅后得到了答案。
Put方法里有一个IndexFor方法会用位运算符(&)比较key的hash值(h)与table的length-1,&比较的是二进制数(当参数是布尔型的时候和逻辑运算符&&有相同的作用只是没有短路功能),当h<length-1,取h值;当h>=length-1,取length-1,当length为2的n次幂时,length-1的2进制位都为1,与h做与运算,能最大程度利用空间,减少冲突。位运算符(&):当对应位置都为1的时候才为1。
(7)我们尽可能少的增加链表的复杂度,因为时间的复杂度为O(n),而空间的复杂度为O(1),理想的状态时每个索引的位置之储存一个Key-Value,这样检索的效率是最高的,但这样消耗的空间也是最大的。
(8)因为数组在内存中占用连续存储的空间,所以扩充的时候得重新定义数组,并将原Entry[] table打散重新计算hash值后均匀的分布到新的数组中,这里要消耗很多的性能,所以我们最好在定义的时候大致确定好数组的长度。
以上是一些总结,如果想要了解更详细的,可以看这篇文章
http://blog.csdn.net/luanlouis/article/details/41576373?utm_source=tuicool&utm_medium=referral#0-qzone-1-52069-d020d2d2a4e8d1a374a433f596ad1440