第一步:new一个HashMap
HashMap<String, Object> map = new HashMap<>();
源码分析如下:
涉及到的成员变量:
final float loadFactor; // 使用的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的加载因子
构造函数就一行,使用默认的加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 使用默认的加载因子数值
// 记得jdk1.8之前构造函数会指定初始容量的,这里并没有,说明在jdk1.8版本中取消了
}
第二步:调用put()方法添加一个元素
map.put("name", "xianyu");
源码分析如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
一共两个方法:里层的hash(key)方法和外层的putVal(......)方法
- hash(key)方法:计算map中key的hash值,源码自行debug跟进,其中需要注意的是:key的hash值是循环key值的每一个字符进行
运算,比如xianyu是6个字符就循环6次进行运算,xianyuxianyu就是循环12次进行运算,所以尽量不要太长,循环次数太多对效率肯定
没有那么友好。
- putVal(hash(key), key, value, false, true)方法:存储map数据过程的方法
涉及到的成员变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // map的初始默认容量,数值为16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // map的默认加载因子
int threshold; // 扩容的阀值,容量达到阀值数之后就要对map进行扩容,其值等于map的容量乘以加载因子
transient Node<K,V>[] table; // 我们都知道map中维护的是Note数组,其实就是这里面的table数组中的数据
先来看一下Note类源码
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 是不是很简单,这个类就4个变量,key的hash值,key值,value值,下一个Note
// 所以Node其实就是一个单链结构,里面存储的才是实实在在真正的数据
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 省略一大堆不需要关心的代码......
}
当我们第一次调用putVal(...)方法时,代码会省略没有调用的部分
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table定义见上方,默认为null赋给tab,所以tab为null,if判断为true进入
if ((tab = table) == null || (n = tab.length) == 0)
// 调用resize()方法给tab数组赋值,然后调用tab数组的长度赋值给n
// resize()方法的作用是对map的Note数组进行初始化并作为返回值赋值给tab,具体实现见下方
n = (tab = resize()).length;
// 调用 (n - 1) & hash 的结果赋值给i,然后把tab数组中下标为i的Note赋值给p并判断是否为null
// (n - 1) & hash 为Note数组长度n和hash值取余的高效率算法
// 此时tab数组中一个元素都没有,所以不管取第几个下标数据肯定都为null,if判断为true进入
if ((p = tab[i = (n - 1) & hash]) == null)
// Note数组中下标为i的Note进行赋值,所以如果tab中取的那个Note为null,就根据数组长度n和hash取余的结果作为此次数组下标进行赋值
tab[i] = newNode(hash, key, value, null);
// 这里省略第一次调用putVal()方法时不会执行的代码一大堆......
++modCount; // 统计次数
// size值加一,并判断,如果size值大于阀值,就需要resize扩容去了
// 首次默认容量为16,默认加载因子为0.75,所以阀值threshold为12,所以当第12次添加元素的时候,++size值为13,
// 13大于12就需要扩容了
if (++size > threshold)
resize();
// 此行代码的实现未做任何处理,是给实现了HashMap的LinkedHashMap去实现使用的
afterNodeInsertion(evict);
// 第一次向map里添加元素就这么简单结束里,添加成功返回值为null
return null;
}
总结:resize()方法对table数组进行初始化,然后根据容量和hash值进行取余得到下标,第一个元素就放在table对应的下标位置
第一次调用resize()方法时,代码会省略没有调用的部分
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // table为null,oldTab也为null
int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap值为0
int oldThr = threshold; // 默认都等于0
int newCap, newThr = 0;
if (oldCap > 0) { // oldCap等于0,不成立,不进入
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0) // // oldThr等于0,不成立,不进入
newCap = oldThr;
else { // 上面的都不成立,所以进入else
// 默认容量16赋值给newCap
newCap = DEFAULT_INITIAL_CAPACITY;
// 默认容量16 乘以 默认加载因子0.75,等于12赋给newThr
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// newThr值为12赋给map的阀值,说明第一次添加元素后确定了阀值为12,只要map的键值对不超过12就不会扩容
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 此处关键,创建一个长度为newCap的Note数组,因为前面newCap为默认容量16,所以此处就创建了一个容量为16的Note
// 数组,然后赋值给table,前面我们知道,table就是map里面真正的数据,所以,这2行代码其实就是给首次给table赋值
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) { // oldTab为null,跳过
// 此处省略一大堆跳过的代码......
}
return newTab;
}
总结:首次执行resize()方法很简单,可以看出,最主要就2个功能,根据默认的容量和加载因子计算出阀值赋给threshold,创建一个容量为16的Note数组赋给table,之后,就可以对这个Note数组进行赋值操作了
其他记录:
扩容操作:
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
这段代码是扩容的操作代码,扩容时,newCap之前的容量 和 newThr之前的阀值 都左移一位等于乘以2,所以每次扩容时,容量和阀值大
小都会变为以前的2倍
扩容时,会重新创建一个容量为新容量的Note数组,然后把之前的旧Note数组的数据一个一个拷贝进去,所以,如果map中要存大量数据,
尽量在初始化的时候指定容量大小,不然频繁的扩容导致频繁的拷贝影响性能
下面扩容代码操作:
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果此Node的next为null,即未形成链表,进入if
if (e.next == null)
// 此Node值e在新数组的下标位置为 自身hash值和新数组容量的取余结果
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 这里为形成链表的Node扩容过程,赋值到新数组中还是链表结构
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//根据e.hash & oldCap 判断节点存放位置
//如果为0 扩容还在原来位置 如果为1 新的位置为 旧的index + oldCap
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
map中的key来决定添加的数据在数组中的位置:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这段代码说明数据在map数组中的位置不是由key的hash值决定的,而是由阀值减1和hash值的取余算法的结果决定的
形成链表 和 链表转为红黑树 的代码:
// 如果此时tab中第i个Node不为空则进入else
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// hash值不相同,key也不相同,所以不进入此if
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// p不属于TreeNode红黑树类型,不进入此 else if
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 进入这里的代码
for (int binCount = 0; ; ++binCount) {
// 因为还没有链表形成,所以Node的next是空的
if ((e = p.next) == null) {
// 关键代码:形成链表了,Node中的next赋值为了新添加的Node数据
p.next = newNode(hash, key, value, null);
// static final int TREEIFY_THRESHOLD = 8; // 固定值为8
// 关键代码:达到TREEIFY_THRESHOLD的值链表即转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
备注:
HashMap在jdk1.8之前采用数组加链表,多线程环境下,会出现死循环,是因为在执行resize扩容操作时,形成链表的时候
采用的是头插法。在jdk1.8之后,采用数组加链表和红黑树结构,采用尾插法,解决了死循环的问题。但是因为没有任何的
同步操作,在多线程put数据的时候依然可能会出现丢失数据等情况。
所以,在多线程环境下,建议使用ConcorrentHashMap,ConcorrentHashMap在put元素和扩容的时候都使用了Synchronized来同步操作
扩展问题:
HashMap<StringBuffer, Object> map = new HashMap<>();
StringBuffer a = new StringBuffer("a");
StringBuffer b = new StringBuffer("a");
map.put(a, "xianyu");
map.put(b, "xianyu");
System.out.println(map);
System.out.println(map.size());
请问:打印出来的map是什么,map.size()等于几?如果你答错的话可以一起交流哦!
网友评论