HashMap源码分析
一。哈希表
Java最基本的数据结构有数组和链表。数组的特点是空间连续(大小固定)、寻址迅速,所以查询快,增加删除慢。
链表恰好相反,可动态增加或减少空间以适应新增和删除元素,但查找时只能顺着一个个节点查找,所以增加删除快,查找慢。
有没有一种结构综合了数组和链表的优点呢?当然有,那就是哈希表
二。HashMap结构
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置,能够很快的计算出
对象所存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,
就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。HashMap底层是通过链表来解决hash冲突的。
1.HashMap中有个Entry数组
transient Entry[] table;
2.Entry的成员变量
final K key; V value; final int hash; Entry<K,V> next; //下一个
3.加载因子
加载因子是表示Hsah表中元素的填满的程度。加载因子越大,填满的元素越多,空间利用率高了,但冲突的机会加大了,则查找的成本越高。
空间与时间关系
4.put
public V put(K key, V value) { if (key == null) //如果键为null的话,调用putForNullKey(value) 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) { //处理冲突的,如果hash值相同,则在该位置用链表存储 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //添加 return null; }
4.1.putForNullKey
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { //如果有key为null的对象存在,则覆盖掉 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); //如果键为null的话,则hash值为0 return null; }
4.2.hash
/** * Applies a supplemental hash function to a given hashCode, which defends * against poor quality hash functions. This is critical because HashMap * uses power-of-two length hash tables, that otherwise encounter collisions * for hashCodes that do not differ in lower or upper bits. */ private static int hash(int h) { // Doug Lea's supplemental hash function h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
4.3.indexFor
static int indexFor(int h, int length) { return h & (length-1); }
这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的n 次方,这是HashMap在速度上的优化。
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h & (table.length-1) hash table.length-1
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
-----------------------------------------------------------------------------------------------------------------------
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101
从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就
产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,
1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,
这意味着进一步增加了碰撞的几率,减慢了查询的效率!
而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,
加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上
形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,
相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
4.3.addEntry
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); //如果要加入的位置已经有值,将该位置的旧值设置为新值的next,也就是新entry链表的下一个节点 if (size++ >= threshold) //是否需要扩容 resize(2 * table.length); //以2的倍数扩容 }
参数bucketIndex就是indexFor函数计算出来的索引值。如果两个key有相同的哈希值,HashMap使用链表来取消覆盖的风险。
4.3.1 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);//用来将原先table的元素全部移到newTable里面 table = newTable; //再将newTable赋值给table threshold = (int)(newCapacity * loadFactor);//重新计算临界值 }
三。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; }
查找过程 : 遍历HashMap的内部维护的Entry数组,找到匹配的目标Entry并返回其value即可。 匹配的标准是:
- 比较Entry的hash值与参数key的hash值。 这要求我们必须正确实现作为key的对象的hashCode()方法
2. 比较Entry的key与参数key是否相等, 这要求我们必须正确实现作为key的对象的equals()方法
示例:
public class TestHashMap { public static <T extends Groundhog> void detectSpring(Class<T> type) throws Exception { Constructor<T> ghog = type.getConstructor(int.class); Map<Groundhog,Prediction> map = new HashMap<Groundhog,Prediction>(); for(int i = 0; i < 10; i++) map.put(ghog.newInstance(i), new Prediction()); System.out.println("map = " + map); Groundhog gh = ghog.newInstance(3); System.out.println("Looking up prediction for " + gh); if(map.containsKey(gh)) System.out.println("--------- found:"+map.get(gh)); else System.out.println("--------- Key not found"); } public static void main(String[] args) throws Exception { detectSpring(Groundhog.class); } } class Groundhog { private int number; public Groundhog(int n) { number = n; } @Override public String toString() { return "Groundhog #" + number; }class Prediction { }
上面的代码不能找到对象
重写hashCode的原因:Object的hashCode()方法使用对象的地址计算散列码。所以两次new出来的对象的hashCode不同。
所以要加上下面的代码
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + number; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Groundhog other = (Groundhog) obj; if (number != other.number) return false; return true; } }