以前看源码的时候,都是草草看过,并没有去认真理解。今天准备把这个过程写下来。
写一个main函数:
Map<String, Person> personMap = new HashMap<String, Person>();
personMap.put("张三", new Person("张三", 22));
personMap.put("李四", new Person("李四", 21));
看看构造函数:newHashMap<>();
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
就是初始化了一个负载因子: DEFAULT_LOAD_FACTOR ,默认为0.75.
static final float DEFAULT_LOAD_FACTOR = 0.75f;
好,看put方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
看hash(key),这个key 就是 我们的String,嗯,无非就是 对象的hashCode()方法,因此我们一定要重写key 的hashCode()方法,因为String已经重写了,所以我们不需要做这一步。得到hashCode,然后做一些无符号位移,然后是或运算。大家可以理解为这是在处理hashKey。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后看 putVal(hash(key), key, value, false, true);
很长,嗯,没关系一点一点看。
我们看到了一个Node<K,V>[] tab; 这个Node是什么,一看,原来是HashMap的内部类。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
其实很简单,一些属性,一个key,一个value,用来保存我们往Map里放入的数据,next用来标记Node节点的下一个元素。
//hashMap的成员变量
transient Node<K,V>[] table;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
第一次put的时候,resize() 会分配一个长度,赋值给n,待会儿在看release(),比较复杂;
关键的代码
因此,newCap=16,newThr = 12;
table 就是HashMap的成员变量。 因此,当我们放入第一个元素时,如果底层数组还是null,系统会初始化一个长度为16的Node数组,这一点像极了ArrayList的初始化。
接下来,
这个hash值是字符串“张三”这个对象的hashCode方法与hashMap提供hash()方法共同计算出来的结果,n是数组的长度,目前数组长度为16,不管这个hash的值是多少,经过(n - 1) & hash计算出来的i 的值一定在0~n-1之间。刚好是底层数组的合法下标,用i这个下标值去底层数组里去取值,如果为null,创建一个Node放到数组下标为i的位置。 i 是Node数组的下标,然后,根据传过来的key,value 新建一个Node对象。就是tab[i] 的值了。
然后就是
modCount 一看就知道记录的是修改次数。
然后,return null ; 因此,hashMap put的时候,只要没有hash冲突,返回的是null。并不是value;这点要注意。
第二次put。
还是我们的putVal();
只要算出来的下标,数组是空的,都会继续往Node里面添加新的元素。
如果,产生了冲突,也就是说,hash算出来的下标,已经有元素了,怎么办?看看hashMap怎么处理。
也就是说new一个新的Node对象并把当前Node的next引用指向该对象,也就是说原来该位置上只有一个元素对象,现在转成了单向链表。
当链表长度到8时,将链表转化为红黑树来处理,
在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率