HashMap是Map最主要实现类,用的非常多,所以对于HashMap的底层实现还是很重要的
阅前须知:
HashMap是线程非安全的,因此效率相较于HashTable会高一些
HashMap存放key-value键值对,且可以存放null的key和value值
HashMap的实现依赖数组+链表+红黑树
HashMap中遇到的概念点:
1. table 存放键值对的数组,数组类型是Node<K,V>
2.threshold 临界值,当存放数据的数组实际长度大于临界值时触发resize()扩容
3.loadFactor 负载因子,用于控制触发数组扩容的系数(当前数组的长度*负载因子=临界值)
- 首先来看看HashMap的构造方法:
/**
* 构造一个具有指定初始容量和负载因子的空HashMap
*/
public HashMap(int initialCapacity, float loadFactor)
/**
* 构造一个具有指定初始容量和默认负载因子的空HashMap
*/
public HashMap(int initialCapacity)
/**
* 构造一个具有默认初始容量和负载因子的空HashMap
*/
public HashMap()
/**
* 通过HashMap构造器放入一个Map直接初始化
*/
public HashMap(Map<? extends K, ? extends V> m)
hashmap拥有上述四个构造方法,前三个实现思路一致,所以主要过一遍是空参构造。
- 创建实例代码
HashMap map= new HashMap();
创建hashMap空参实例时,会默认负载因子为0.75,但并未初始化容量(jdk8中)
/**
* 未指定负载因子的值时的默认值为0.75.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 需要注意的是jdk8未初始化容量大小而是当首次使用put方法时才给默认值,但jdk7时默认给了初始化值为16
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 这里只是设置负载因子为默认值,未初始化容量,
}
- put()方法
map.put("key","value");
使用put()方法添加数据时,调用putVal()
public V put(K key, V value) {
/**
* hash(key), key的Hash值
* key, 要存放的key值
* value,要存放的key值
* onlyIfAbsent 为true时,不会覆盖已存在的key所对应的value值
*/
return putVal(hash(key), key, value, false, true);
}
进入putVal方法后,首先判断table是否为空,若为空就调用resize()方法,初始化长度为16,临界值为16*0.75=12
/**
* 当table(Node数组)为null或长度为0时,调用resize()方法,初始化数组长度
*
*/
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
/**
* 调用resize()方法,判断table为空,将数组当前大小赋值为0,且初次调用put()时threshold也为0,
*/
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
/**
* 此时代码会进入赋值操作,赋值初始容量为16,临界值为16*0.75
*/
else {
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//0.75*16
}
继续往下走,通过(n-1) & hash 计算出的值作为tab的下标i,并把tab[i]的引用指向新的数组p,如果p为null,则调
用NewNode()创建该链表的第一个节点并赋值给tab[i]
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
/**
* newNode方法
*/
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
如果P不为null,当P的hash值与当前要添加的key的hash值、P的Key值与要添加的key相等时,判断两者key相同,将p的引用指向e,如果p是红黑树结构,那么插入后仍是红黑树结构
//n为tab的长度,i为tab的下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//定义即将插入的节点
Node<K,V> e; K k;
//若判断e节点key的hash值与p节点相等且key值相等或符合equals方法,则
//判断两者key相同,将p的引用指向e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//p表示tab上的某Node节点
else if (p instanceof TreeNode)
//若添加的节点时红黑树结构,则调用putTreeVal,同样得到的也是红黑树结构
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//当p.next为空时说明链表到头了,直接将新加节点的引用给P.next
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//当链表的长度大于8时,调用treeifBin方法,这个方法的作用是判断是否
//要将链表转换成红黑树结构,当链表长度超过8但是不超过64时,会对链表
//调用resize()进行扩容,当超过64时,才会将链表转换成红黑树结构
treeifyBin(tab, hash);
//如未超过则break
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
继续走下一步,针对已存在key进行的处理,若OnlyifAbsent为true或原有节点的value为空时,将插入节点的value值设置为已存在节点的value,返回原有的value值
if (e != null) { // 当key已存在时
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//未做任何实质性操作
return oldValue;
}
最后,当数组长度超过临界值时调用resize()对数组进行扩容,默认新数组是原有数组的2倍
++modCount;
if (++size > threshold)
resize();//扩容
afterNodeInsertion(evict);//无实质作用
return null;
最后,推荐一篇我觉得写得很好的博客
hmi1024的博客 Java8 HashMap源码的简单分析(1)