首页 > 代码库 > HashMap源码及原理解析
HashMap源码及原理解析
1、HashMap简介
HashMap提供所有可选的Map操作,并允许使用 null 值和 null 键,是线程不安全的。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目(或者说元素)数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
默认的加载因子是0.75,是寻求时间复杂度和空间复杂度的平衡。加载因子过高,空间利用率高,但是会增加查询成本,在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点(后面会详细解释)。
另外,HashMap实现了实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
2、核心源码分析
以下是JDK1.6的源代码:public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //默认的初始容量(初始数组的长度)是16,且必须是2的整数次幂, static final int DEFAULT_INITIAL_CAPACITY = 16; //数组的长度范围是[0,2的30次方], 小于0会抛异常,大于MAXIMUM_CAPACITY会被它覆盖。 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //存放数据的Entry数组,实际容量必须是2的幂 //每个Entry元素其实是一个链表 transient Entry[] table; //HashMap中已经存放数据的个数,并非一定是数组的长度 transient int size; //HashMap的阈值,如果size>threshold(threshold = 容量 * 加载因子)则HashMap需要rehash int threshold; //加载因子 final float loadFactor; //HashMap被改变的次数 transient volatile int modCount; }
我们在看看Entry的结构,只截取了重要的部分
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * 创建新的Entry,并让其next指针指向n */ Entry(int h, K k, V v, Entry<K,V> n) { value = http://www.mamicode.com/v;>
下图是HashMap的数据结构图,当两个entry的key经过算法获得的index相同,但value不同时,就采用链表解决冲突。
2.1 put方法
现在我们来看看put方法的源码: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 = http://www.mamicode.com/e.value;>
首先,我们看看put的流程图(建议与文字说明对照阅读):
详细的流程如下:
1、检测key是否为null,如果是null,会被存放到以table[0]为头节点的链表中(null的hash值总是0),因为存在冲突,table[0]处可能已经有了别的元素。
2、调用key的hashCode()得到哈希值,然后再调用hash()重新计算哈希值,再通过indexFor()得到该key所对应的index,下面来看下hash()和indexFor()的源码:
//求hash值的方法,重新计算hash值 static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } // 返回h在数组中的索引值,这里用&代替取模,旨在提升效率 static int indexFor(int h, int length) { return h & (length-1); }
hash()就不讨论了,这里我们仔细分析下indexFor(),细心的同学会发现传递过来得length是容量的大小,而容量的大小是强制要求为2的幂次方。这样就保证length-1是2^n-1,其二进制表示为1111(N个1),任何一个数字与length-1进行与运算(&)的结果必定是分散在0到length-1之间。如此,我们才能保证已保存的entry是分布均匀的,空间是足够被利用的。例如
h | length – 1 | 二进制 | h&length-1 |
0(0000) | 1111 | 0000 | 0 |
1(0001) | 1111 | 0001 | 1 |
12(1100) | 1111 | 1100 | 12 |
16(10000) | 1111 | 0000 | 0 |
20(10100) | 1111 | 0100 | 4 |
3、 如果发现key已经存在,则覆盖原来的值,返回旧值。
注意:HashMap中全部是通过equels()来判断两个对象(key和value)是否相等。在判断key是否存在时,这里是有加速优化的,首先判断key的hash值是否相等,在相等的前提下再去判断key是否相等。原因:enrty中已经保存了hash,而key的hash值只用计算一次,并且hash值不相等的两个对象的值一定不相等,所以先判断hash值是否相等,比直接判断key.equals(k)来的快。
4、如果没有就调用addEntry将新元素插入所在链表的头节点处。另外,addEntry也还有很多需要注意的地方,其源码如下:
//添加新Entry元素 void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) //如果size>阈值,则将数组长度扩大成原来的两倍 resize(2 * table.length); } //调整大小 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); } //转移Entry[]数组 void transfer(Entry[] newTable) { Entry[] src = http://www.mamicode.com/table;>
我们发现当size>threshold(阈值)时,需要将数组长度扩大成原来的两倍,是调用resize()来实现的,而resize()又是通过调用transfer()来实现。transfer()中做了两件事情:一件是重新计算元素的index,一件是将其拷贝至新数组,显然这是非常耗时的。所以,我们在使用HashMap时,最好预先估算待存储元素的个数,避免resize的发生,这样有助于提高HashMap的性能。2.2 get()
当我们了解了put的原理后,再来看看get的源码及其原理:public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; } private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }1、首先判断key是否为null,如果是则遍历以table[0]为头节点的链表,如果e.key==null,则返回其value;
2、再次通过hash()重复计算key的哈希值,并通过indexFor来计算index值,然后遍历table[index]为头节点的链表,找到value;
好了,到此我们已经了解了HashMap的put和get,但是还有一个问题我们没有解释,为什加载因子过高,虽然减少了空间开销,但同时也增加了查询成本?因为,无论Hash函数如何设计,所有的Hash表理论上都不可能避免冲突。当table中快填满时(加载因子大,已存在的元素多),再填入新的元素,冲突的概率会增大。当put时产生冲突(用一个链表解决冲突),get和下一次put就有可能需要去遍历链表,查询成本自然就高了。理想情况是没有链表,所有的元素均存在table上,因此,要减少冲突的机会。
HashMap源码及原理解析
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。