1,数据结构
hashmap作为key-value集合,理想效果是能达到O(1)复杂度。
结构图:
数据结构HashMap:
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable { static final int DEFAULT_INITIAL_CAPACITY = 16; static final int MAXIMUM_CAPACITY = 1073741824; static final float DEFAULT_LOAD_FACTOR = 0.75F; transient Entry[] table;//这里就是用来存K-V的数组 transient int size; int threshold; final float loadFactor; volatile transient int modCount; private transient Set<Map.Entry<K, V>> entrySet = null; private static final long serialVersionUID = 362498820763181265L; 。 。 。 }
数据结构Entry:
static class Entry<K, V> { final K key; V value; Entry<K, V> next;//key.hashcode()值相同的下一个节点 final int hash; Entry(int paramInt, K paramK, V paramV, Entry<K, V> paramEntry) { this.value = paramV; this.next = paramEntry; this.key = paramK; this.hash = paramInt;//hash(key.hashCode())的值,其实是经过了两次hash后的值 } public K getKey(); public V getValue(); public V setValue(V paramV); public boolean equals(Object paramObject); public int hashCode(); }
2,hashmap的工作原理
接下来就是put(key, value)和get(key)过程,为了通过放入的过程(put)来理解取出(get)的过程,这里先说put(key, value):
put(key, value)过程,源码和注释:
public V put(K paramK, V paramV) { if (paramK == null) return putForNullKey(paramV); int i = hash(paramK.hashCode());//第二次hash处理 //i&this.table.length-1求出下标 int j = indexFor(i, this.table.length); //下标j就是K在Entry数组中的位置bucket,遍历Entry链表,若K存在(注意K //引用相同或者equals()条件满足都视为存在)放入V并返回原值;否则新增 for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next) { Object localObject1; if ((localEntry.hash != i) || (((localObject1 = localEntry.key) != paramK) && (!paramK.equals(localObject1)))) continue; Object localObject2 = localEntry.value; localEntry.value = paramV; localEntry.recordAccess(this); return localObject2; } this.modCount += 1; //新增Entry节点(这里是在bucket位置从链表头部插入,常数复杂度) addEntry(i, paramK, paramV, j); return null; }
get(key)过程,源码和注释:
public V get(Object paramObject) { if (paramObject == null) return getForNullKey(); //跟put方法一样,先根据K求出下标,找到bucket位置,然后遍历Entry链表 int i = hash(paramObject.hashCode()); for (Entry localEntry = this.table[indexFor(i, this.table.length)]; localEntry != null; localEntry = localEntry.next) { Object localObject; //此处就是K要尽量使用持久化对象的原因哦,K的hashcode变了或者equals方法不 //满足就找不到啦,也是我们要同时复写K的hashCode()和equals方法的原因 if ((localEntry.hash == i) && (((localObject = localEntry.key) == paramObject) || (paramObject.equals(localObject)))) return localEntry.value; } return null; }
通过以上源码分析可以看出在bucket不太大的情况下put(key, value)和get(key)操作都是O(1),文章开头之所以说是理想效果就是考虑到了最坏的情况,当bucket无限增大,导致所有Entry都挤在一个bucket里面,那么每一次put(key, value)和get(key)都是O(n)复杂度,HashMap也就没意义了!一个好的hash算法是兼顾性能和碰撞率的,性能就是hash值的计算过程不能太耗时间,碰撞率在这里说的就是让Entry尽可能的离散,不要挤在一个bucket里面。这里从两个方面来说碰撞率:
①hash算法
常用的作为K的String.hashCode计算:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度
第二次hash:
//paramInt为key.hashCode()的值 static int hash(int paramInt) { paramInt ^= paramInt >>> 20 ^ paramInt >>> 12; return paramInt ^ paramInt >>> 7 ^ paramInt >>> 4; }
计算方法挺简单的,就是几次无符号移位以及异或运算,然而我并不明白其中的奥妙!
②table也就是Entry数组的大小以及rehash
这里其实想说的就是加载因子loadFactor(默认0.75),也就是Entry数(this.size)达到this.table.length*0.75就要rehash了,
HashMap构造函数:
//paramInt初始容量 paramFloat 加载因子 public HashMap(int paramInt, float paramFloat) { if (paramInt < 0) throw new IllegalArgumentException("Illegal initial capacity: " + paramInt); if (paramInt > 1073741824) paramInt = 1073741824; if ((paramFloat <= 0.0F) || (Float.isNaN(paramFloat))) throw new IllegalArgumentException("Illegal load factor: " + paramFloat); int i = 1; //可以看到table.length的初始值是大于paramInt的最小的2的对数 while (i < paramInt) i <<= 1; this.loadFactor = paramFloat; this.threshold = (int)(i * paramFloat); this.table = new Entry[i]; init(); }
在put操作中,新增节点调用addEntry函数:
void addEntry(int paramInt1, K paramK, V paramV, int paramInt2) { Entry localEntry = this.table[paramInt2]; this.table[paramInt2] = new Entry(paramInt1, paramK, paramV, localEntry); //条件满足的话,rehash if (this.size++ >= this.threshold) resize(2 * this.table.length);//扩大为2倍 } //调整table为两倍大小 void resize(int paramInt) { Entry[] arrayOfEntry1 = this.table; int i = arrayOfEntry1.length; if (i == 1073741824) { this.threshold = 2147483647; return; } Entry[] arrayOfEntry2 = new Entry[paramInt]; transfer(arrayOfEntry2); this.table = arrayOfEntry2; this.threshold = (int)(paramInt * this.loadFactor); } //rehash void transfer(Entry[] paramArrayOfEntry) { Entry[] arrayOfEntry = this.table; int i = paramArrayOfEntry.length; for (int j = 0; j < arrayOfEntry.length; j++) { Object localObject = arrayOfEntry[j]; if (localObject == null) continue; arrayOfEntry[j] = null; //遍历原table每一个bucket,计算Entry在新table的下标,并将Entry仍然从bucket //头部插入,可以看到rehash后每个bucket会被翻转一次 do { Entry localEntry = ((Entry)localObject).next; int k = indexFor(((Entry)localObject).hash, i); ((Entry)localObject).next = paramArrayOfEntry[k]; paramArrayOfEntry[k] = localObject; localObject = localEntry; } while (localObject != null); } }
从这里也可以看出HashMap的rehash是单线程的,不是一个线程安全类,在rehash过程中另一个线程进行get(key)操作可能会出现数据不一致问题。
第一次写博客,说的也都是很基础的东西,不足之处希望大家多多指教。