一,引言:HashMao基于jdk1.8的底层数据结构:

数组的查询效率为O(1),链表的查询效率是O(k),红黑树的查询效率是O(log n),所以当元素量很大时,红黑树的效率就体现出来了。
二,HashMap的构造方法
1,无参构造:
HashMap<String, String> map = new HashMap<String, String>();
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
//加载因子--0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
无参构造时,HashMap自动构造了一个默认的加载因子0.75;(这个加载因子的最大容量为16)
因子的大小表示每一个Hash桶中所能填满元素多少,因子越大,所能填满的元素越多;好处是,空间的利用率增加了;坏处是,冲突的机会加大了。
由于HashMap遇到冲突便会转换为链表储存或是红黑树储存,当需要查询一个数据时,就会增加查询次数,增加查询的成本。
为什么默认的加载因子为0.75?原因是因为因子大了,冲突的可能就会增加,而因子小了,就会使空间浪费更多,并且会增加扩容的操作次数;所
以在“冲突增加”与“空间利用”之间,寻找一种平衡与折中,这就是为什么因子为0.75。
HashMap的默认容量是:0.75*16=12;
2,带参构造:
//构造一个容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
//进行判断,如果传入参数小于0,抛出异常。
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
//如果传入参数大于容量最大的参数,默认设置为最大容量。
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
//isNaN:is not a Number;
//判断加载因子参数,isNaN(i):返回一个Boolean值,如果i为NaN值,那么isNaN返回true,反之返回一个false。
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//当因子参数小于等于0或不为NaN值时,抛出异常。
this.loadFactor = loadFactor;//加载因子赋值
//阈值---控制扩容---起到限制容量的作用,扩容阈值threshold = loadFactor*capacoty
this.threshold = tableSizeFor(initialCapacity);//
//tableSizeFor这个方法是找到一个离容量参数最近的2的n次方数例,比如cap = 14,return 16;
}
3,tablesSizeFor方法:
static final int tableSizeFor(int cap) {
//-1可以保证当传入的数刚好是2的次方时,可以正确的返回其本身,例:传入的是16,经过下面的计算后还是返回16
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//这是HashMap默认的规则,其容量大小(桶的数量)必须为2的n次方数。原因是:
//HashMap为了存取的高效,就尽量把数据分配均匀,关键就是判断把当前数据存放到哪一个桶中,这个算法就是取模运算。
//当前key的Hashcode % HashMap的容量,但是由于取模运算的效率不如位运算(&),位运算就是根据二进制按位运算,1遇1
//得1,其他为0,比如1100 & 1010 = 1000;这样明显逻辑更为简单,更加快速。
//当容量为2的n次方时,hashcode&(HashMap的容量 - 1)==hashcode%HashMap的容量;于是位运算替换了取模运算,而前提就是容量为2的n次方。
三,HashMap的put方法
map.put(k,v);
↓
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//put进一组kv时,当kv存进数组后,会返回一个null;如果过程为替换,则会把被替换的值返回。
//根据传过来的key-调用他hashCode方法,通过高16和低16位的异或运算增加随机性
//计算key的hashcode值进行比较或是选择放进哪一个hash桶中
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//定义了几个局部临时变量
Node<K,V>[] tab; //数组可以暂存所有hash桶
Node<K,V> p;//暂存一个桶中的节点或链表
int n, i;
//transient Node<K,V>[] table;--成员变量--这个就是hashMap的底层实现,相当于
//ArrayList里面的elementData。
//扩容
//↓
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//当put数组的长度达到了扩容的阈值threshold之后,调用resize()方法进行扩容。
//扩容逻辑:如果是带参构造:1,将用户传入的容量参数*用户传入的负载因子loadFactor的计算结
//赋值给扩容阈值threshold。2,将容量参数赋值给新的容器,根据新容器创建出一个新的node数组
//无参构造:1,将 默认负载因子0.75f * 默认的容量大小DEFAULT_INITIAL_CAPACITY,的计算结果,赋值给扩容阀值threshold。
//2,将默认容量大小DEFAULT_INITIAL_CAPACITY(16)赋给新的Node数组;根据新的Node数组创建出一个新的Node数组。
//n中存的是扩容后临时的tab的长度。
if ((p = tab[i = (n - 1) & hash]) == null)
//如果这个桶里位空,直接放入桶中的第一个位置。
//tab[15&hash]位运算计算key放到哪个hash桶里。(接上面说的为什么使用位运算)
//如果这个桶不为空。
//↓
else {
//hash冲突了----一个节点,一个链表,红黑树
Node<K,V> e; //暂存一个节点---重复的节点
K k;//键
// 如果桶中第一个元素的key与待插入元素的key相同,保存在e中e来记录,后续通过e判断是否直接return
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//说明跟桶中的key值重复了。
e = p;
// 如果这个p是树节点,则调用树节点的putTreeVal插入元素
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//说明是链表
//遍历这个桶对应的链表,binCount用于存储链表中元素的个数
else {
for (int binCount = 0; ; ++binCount) {
//链表遍历完后都没有找到相同的key的话,说明该key对应的元素不存在,则在链表末接上一个新的节点。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//这里判断插入新节点后,链表长度是否大于等于8,判断是否需要树化,因为第一个元素没有加到binCount中,所以这里-1
//如果数组的长度没有达到64,还是会继续扩容,不会树化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//树化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果不为空,说明这个节点的Key重复---就不会添加一个元素,而是替换
if (e != null) { // existing mapping for key
//记录下旧值
V oldValue = e.value;
//判断是否替换旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;//替换
afterNodeAccess(e);//返回旧值
return oldValue;
}
}
//modcount为修改次数,不论是增加或者删除,都会++。
++modCount;
if (++size > threshold)
resize();//元素数量++,判断是否达到扩容阈值,大于扩容阈值时,调用resize方法进行扩容。
afterNodeInsertion(evict);
return null;//没找到元素返回null。
}
四,HashMap put方法逻辑思维导图

五,树化的方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//链表要转红黑树得让hash桶的长度达到64
//static final int MIN_TREEIFY_CAPACITY = 64;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {//树化
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
红黑树待更新。