在这里插入代码片
## HashMap的定义
·HashMap 是基于哈希表的 Map 接口的实现的,实现了所有map的操作,允许key和value为null,但是key不能重复
HashMap的基本属性
// 默认的初始容量必须是2的幂 默认值为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
// 最大容量 容量的值必须是2的幂 最大值为2的30次幂
static final int MAXIMUM_CAPACITY = 1 << 30
//负载因子默认值为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f
// 链表长度大于8的时候 转为红黑树结构
static final int TREEIFY_THRESHOLD = 8
// 长度小于等于6时转为链表
static final int UNTREEIFY_THRESHOLD = 6
// 树形结构时最小的容量
static final int MIN_TREEIFY_CAPACITY = 64
注意:
1.容量、负载因子、元素个数关系
·容量capacity:表示可以装多少个元素
·元素个数size: 表示装了多少元素了
·LoadFactor:负载因子,默认值0.75f
如当前容量大小为32 当存入了第25(32*0.75)个元素时,
hashmap就会进行resize(扩容)了
元素在HashMap中的位置计算
hashmap在通过key的hashcode方法计算值之后,会调用indexfor方法将hashcode值转为数组的下标: h & (length-1) Java利用位运算来取代取模操作,因为位运算直接对内存数据进行操作,不需要转成十进制,之所以可以做等价代替,前提是要求HashMap的容量一定要是2^n
HashMap的数据结构
1.7 底数组+链表 采用头插法
1.8 数组+链表+红黑树 采用尾插法
HashMap 底层的数据是数组被称为哈希桶,每个桶存放的是链表,链表中的每个节点,就是 HashMap 中的每个元素。在 JDK 8 当链表长度大于等于 8 时,就会转成红黑树的数据结构,以提升查询和插入的效率
https://img-blog.csdnimg.cn/20201028143455147.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NjAzNTA1,size_16,color_FFFFFF,t_70#pic_center
重要方法
1.put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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为空或长度为0,调用resize方法进行扩容创建新的数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算该键值对在数组中的存储位置,此位置为空则进行插入操作
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 (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 该链为树,新增一个树街店
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//链表节点
//遍历,直到链表末尾,添加新节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
// 若hash与key相同,则终止便利遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 将节点的引用后移
p = e;
}
}
/ 存在key对应的value时,按照条件替换并返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
https://img-blog.csdnimg.cn/20201028160342612.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NjAzNTA1,size_16,color_FFFFFF,t_70#pic_center
2. get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 该方法是 Map.get 方法的具体实现
* 接收两个参数
* @param hash key 的 hash 值,根据 hash 值在节点数组中寻址,该 hash 值是通过 hash(key) 得到的
* @param key key 对象,当存在 hash 碰撞时,要逐个比对是否相等
* @return 查找到则返回键值对节点对象,否则返回 null
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 声明节点数组对象、链表的第一个节点对象、循环遍历时的当前节点对象、数组长度、节点的键对象
// 节点数组赋值、数组长度赋值、通过位运算得到求模结果确定链表的首节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点
((k = first.key) == key || (key != null && key.equals(k))))
return first; // 返回首节点
// 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对
if ((e = first.next) != null) {
// 如果存在下一个节点 e,那么先看看这个首节点是否是个树节点
if (first instanceof TreeNode)
// 如果是首节点是树节点,那么遍历树来查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果首节点不是树节点,就说明还是个普通的链表,那么逐个遍历比对即可
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 比对时还是先看 hash 值是否相同、再看地址或 equals
return e; // 如果当前节点e的键对象和key相同,那么返回 e
} while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环
}
}
return null; // 在比对完了应该比对的树节点 或者全部的链表节点 都没能匹配到 key,那么就返回 null
3. 扩容
·计算新容量:根据老数组的容量确定扩容后的容量值,一般扩容为老容量的2倍
·创建新数组:根据新的容量创建数组,需要尽量避免扩容的发生,因为产生新的数组必然是会消耗一定的内存空间。
·元素放置到新数组:循环遍历老数组,将元素重新放置到新数组中