JDK1.7和JDK1.8HashMap差别很大。
本次主要讨论JDK1.7的HashMap。
1、原理(原理采用 数组+链表存储数据 原理如下图)
对于一个元素的插入,首先要考虑它在数组中的位置。 常用散列函数存放其位置。
常用的散列函数的设计 由以下几种
1、直接赋值法(是多少就放数组中那个位置 如100就放数组中第100个位置。)
2、数据分析法 (根据一个数据进行分析如:身份证430521199810143314 直接取后四位3314 存放数组位置3314 )
3、平方取中法(一个数据进行平方进行存放)
4、取余法(如 上图中的28 28%数组长度 8 -》》4 所以存放数组第四个位置。)
HashMap中也采用的是取余法。
Hash算法有一个弊端,会造成冲突。也叫Hash冲突。
如果二个元素取余结果相同(如上图中的 28、4 )这时候就会造成Hash冲突
Hash冲突有几种常见的解决方法。
1:开发定值法:对于冲突的元素往数组 前移或者后移一位。或者移动多个位置(缺点,容易导致数据集合在一块)
2:链表法。
HashMap采用的是链表法,把Hash冲突的数据 用链表依次存储。
2、源码分析(只关注常见的方法和成员变量)
上面说到 HashMap主要采用 数组加上链表 存储数据。
//数组
transient Entry<K,V>[] table;
//链表 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构 int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
一些关键的成员变量
//实际存储的key-value键值对的个数 transient int size; //负载因子,代表了table的填充度有多少,默认是0.75 final float loadFactor; //threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold int threshold;
HashMap有四个构造函数
public HashMap(int initialCapacity, float loadFactor) { //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230) if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init();//init方法在HashMap中没有实际实现 }
initialCapacity默认为16,loadFactory默认为0.75
接下来看一下Put操作
public V put(K key, V value) { //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16) if (table == EMPTY_TABLE) { inflateTable(threshold); } //如果key为null,存储位置为table[0]或table[0]的冲突链上 if (key == null) return putForNullKey(value); int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀 int i = indexFor(hash, table.length);//获取在table中的实际位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value 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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败 addEntry(hash, key, value, i);//新增一个entry return null; }
inflateTable方法
private void inflateTable(int toSize) { int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂 toSize为12 返回 的是16 toSize为16返回的为16 toSize为 17返回的是32 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
hash函数
//用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
/** * 对hash取模 返回数组下标 等同于 h%(length) 只不过效率更高。 */ static int indexFor(int h, int length) { return h & (length-1); }
h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=25 换成二进制计算为9。
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
数组扩容
resize()
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, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
我们先来看看transfer这个方法
如果数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),index也可能会发生变化,需要重新计算index
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去 for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity);//根据新数组的大小 下标计算出重新存入的 位置。 //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。 e.next = newTable[i]; newTable[i] = e; e = next;//链表向后 } } }
get方法
public V get(Object key) { //如果key为null,则直接去table[0]处去检索即可。 if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
getEntry方法
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //通过key的hashcode值计算hash值 int hash = (key == null) ? 0 : hash(key); //根据key 找到对应存储链表 遍历链表 根据key找到 相应元素 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 != null && key.equals(k)))) return e; } return null; }