探究HashMap的工作原理
GeorgiaToma
8年前
<p>摘要:经过大学四年,我也刚刚毕业了3个月了,学习android也有一两年了,可是在大学的生活中,磨练出来的解决实际问题的能力是有的,完成开发任务还是可以的,但是总觉得技术遇到了瓶颈期,没有办法进步很多。因此想静下心来看JDK的源码,看看这些人是怎么设计出来的?为什么要这么设计?我能从中体会到什么?对我今后的职业生涯有什么帮助?跟着优秀的人一起成长吧。</p> <p>我们用HaspMap最关心的两个方法和几个变量是:</p> <pre> <code class="language-java">/** 默认的初始化容量,2^4 */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** 默认的负载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /*数组,Entry是存放Key value的基础bean */ transient Entry[] table; /** 负载因子*/ final float loadFactor; /** *下一次的负载table容量 (capacity * load factor) * int threshold; public V put(K key,V value) public V get(Object key)</code></pre> <p>为了帮助理解和记忆,贴上两张图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/bcf9c4ef67b0a0dd35e425ad8cdf9c2c.png"></p> <p style="text-align:center">Paste_Image.png</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/2269d168cdb5485e09c75fa72253dc02.png"></p> <p style="text-align:center">Paste_Image.png</p> <p>从上图我们可以发现哈希表是由 <strong>数组+链表</strong> 组成的,一个长度为 我们看到的第一个变量 <strong>DEFAULT_INITIAL_CAPACITY=16</strong> 的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key.hashCode()%table.length)获得,也就是元素的key的哈希值对数组长度取模得到。</p> <p>HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。HashMap里面实现一个静态内部类Entry,其重要的属性有key,value,next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。</p> <h3><strong>HashMap存取过程</strong></h3> <pre> <code class="language-java">public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }</code></pre> <p>看源码得出几个结论:</p> <p>1.HashMap可以存储null的key。</p> <p>2.通过key.hashCode找到hash值,然后跟table.length的取模,得到数组下标。然后遍历链表,如果hash值且key值相等,则替换原来的值。</p> <p>3.table添加新的元素时并没有奇怪,奇怪的是我们知道数据的默认容量是16,超过了16怎么办,HashMap是怎么处理的呢?</p> <p>所以要看一下上述代码中addEntry这个方法里面到底做什么操作。</p> <pre> <code class="language-java">void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }</code></pre> <p>到这里已经很明显,我们发现了HashMap的处理方式,是size > threshold的时候就会去扩容。那么这个threshold是什么东西呢?文章的开头我们已经把他列出来了,threshold = capacity <em>loadFactor的结果,loadFactor是负载因此,capacity是容量。那么我们可以得出很简单的结论就是首次初始化的时候threshold = DEFAULT_INITIAL_CAPACITY(16)</em> DEFAULT_LOAD_FACTOR (0.75) ,就是超过了容量的75%的时候就会去扩容。</p> <p>当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。</p> <p>我带着好奇心继续探索resize这个方法。</p> <pre> <code class="language-java">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; //重新计算index int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }</code></pre> <p>到这里基本上清楚了HashMap的进行put时候的整个过程了。相同get方法我就不贴代码了。有兴趣可以去研究源码。</p> <p>当我看源码看到这一步我可能比较困惑,HashMap究竟优势在哪儿?</p> <p>我们要分析一下HashMap的数据结构,上述我们已经了解HashMap的数据结构是 <strong>数组加链表。</strong></p> <p>数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;</p> <p>链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表 <strong>的特点是:寻址困难,插入和删除容易。</strong></p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/006f9f446fcb</p> <p> </p>