HashMap有两个初始化因子:初始容量和加载因子,默认的初始容量为16,加载因子是0.75,也就是说当map中存放的对象超过16*0.75=12个时,hashMap会做一次resize操作,使当前容量翻倍。由于hashmap存放的entry是使用数组存放,所以当hashmap中存放的对象非常多时,resize操作性能消耗还是比较可观的,比如容量已经到10w时,一次resize操作会初始化一个20w的数组。
代码解读:
如前所述,hashmap是使用数组存放数据,而这个数组存放的是Entry对象,Entry对象是一个单链表,里面存放了key,value和指向下一个Entry的引用。当冲突发生的时候当前Entry会替换数组中的位置,并且next指向之前的旧值。这样就完成了单链表的更新。
put:
public V put(K key, V value) { //hashMap允许存在一个空键作为key if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //hash出来的值作为下标查询数组,如果该位置为空,直接插入,如果该数组不为空,则说明发生冲突,冲突包括两种:key已经存在,则直接替换,并返回旧值;key不存在但发生了hash冲突 for (Entrye = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; //仅仅是value替换,单链表的结构并不改变 e.value = value; e.recordAccess(this); //注意:返回的是旧值 return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;}
上面有一个hash()方法,作用是通过对key对象的哈希值进行按位异或运算计算出哈希散列,下面这篇文章比较详细地阐述了算法的优点。
get:
get方法代码相对简单,先计算出该key的hash值,以此值作为数组下标查询数组,然后遍历该Entry指向的单链表。找到就返回,找不到就返回null
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); //该步骤类似于put中查找存在冲突的key的过程,不一样的是put中找到就替换,get中找到就返回value for (Entrye = 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;}
entrySet:
entrySet方法返回一个包含map中所有Entry的Set,通过这个Set可以对map中映射的对象进行遍历操作,代码比较简单。我以前有个坏习惯就是先使用hashmap的keySet方法,然后遍历这个Set,在遍历中再用hashmap的get方法取值,后来看了hashmap的源码之后才发现这种方式相当于比直接entryset多了一次计算,在数据量小的时候体会不到性能差异,当数据量比较大时,就能感觉到差异了。