了解hashmap首先要从它的结构说起,hashmap的数据结构为数组+链表+红黑树,当链表的长度大于8且数组大小大于等于64时会将链表转化为红黑树。下面重点说一下put方法,从别的地方拿来了一张流程图,配合流程图更加容易理解。
下面解读一下put方法的源码:
public V put(K key, V value) {
//调用putVal方法,并计算key的hash值传入到putVal方法中
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// i为数组下标 p为数组项中存放的链表
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
//判断table是否为空,table对象是node数组,即hashmap中数据+链表+红黑树中的数组
if ((tab = table) == null || (n = tab.length) == 0)
//对hashmap的临界值和数组进行初始化
n = (tab = resize()).length;
//计算当前对象存放的下标,并判断数组中该下标是否已经存值
if ((p = tab[i = (n - 1) & hash]) == null)
///如果不存在则在该位置创建一个新的链表。并将目标值存入新链表的表头
tab[i] = newNode(hash, key, value, null);
else {
HashMap.Node<K,V> e; K k;
//判断目标key的hash值与链表头节点的hash值是否相等,它们的key是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断p节点的类型是否是TreeNode
else if (p instanceof HashMap.TreeNode)
//如果红黑树节点中不存在与目标key相同的值,将目标对象插入红黑树中,存在相同的key则返回已存在的对象
e = ((HashMap.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);
//判断链表长度是否大于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//将链表转化为红黑树
treeifyBin(tab, hash);
break;
}
//遍历的过程中发现链表中有key与目标对象的key相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为空说明,hashmap中已经存在了当前目标对象的key,则将value进行覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//修改值后,返回旧值
return oldValue;
}
}
//记录HashMap结构被修改的次数
++modCount;
//判断数组大小是否已经大于临界值了
if (++size > threshold)
//进行扩容
resize();
afterNodeInsertion(evict);
//Hashmap中不存在key,所以返回null
return null;
}
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
int n, index; HashMap.Node<K,V> e;
//如果数组大小小于64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//进行扩容
resize();
//链表结构转红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) {
HashMap.TreeNode<K,V> hd = null, tl = null;
do {
HashMap.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);
}
}
查看完源码,再看一下demo,配置debugger跟踪查看hashMap的put方法效果更佳:
HashMap<HashValTest, String> stringHashMap = new HashMap<>();
HashValTest test1 = new HashValTest("zcs1", 1);
HashValTest test2 = new HashValTest("zcs2", 2);
HashValTest test3 = new HashValTest("zcs3", 3);
HashValTest test4 = new HashValTest("zcs4", 4);
HashValTest test5 = new HashValTest("zcs5", 5);
HashValTest test6 = new HashValTest("zcs6", 6);
HashValTest test7 = new HashValTest("zcs7", 7);
HashValTest test8 = new HashValTest("zcs8", 8);
//链表长度大于8,调用treeifyBin()方法尝试将链表转化为红黑树,但是当前数组大小为16,没有达到64,所以将数组大小扩容为32
HashValTest test9 = new HashValTest("zcs9", 9);
//链表长度大于8,调用treeifyBin()方法尝试将链表转化为红黑树,但是当前数组大小为16,没有达到64,所以将数组大小扩容为64
HashValTest test10 = new HashValTest("zcs10", 10);
//链表长度大于8,调用treeifyBin()方法尝试将链表转化为红黑树,此时数组大小为64,以满足条件要求,成功将链表转化为红黑树。
HashValTest test11 = new HashValTest("zcs11", 11);
stringHashMap.put(test1, "1");
stringHashMap.put(test2, "1");
stringHashMap.put(test3, "1");
stringHashMap.put(test4, "1");
stringHashMap.put(test5, "1");
stringHashMap.put(test6, "1");
stringHashMap.put(test7, "1");
stringHashMap.put(test8, "1");
stringHashMap.put(test9, "1");
stringHashMap.put(test10, "1");
stringHashMap.put(test11, "1");
System.out.println(stringHashMap);
对应的实体类:
/**
* @author zhangchangsi
* @version 1.0
* @date 2022/3/17 23:30
*/
public class HashValTest {
private String name;
private int age;
public HashValTest() {
}
public HashValTest(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashValTest that = (HashValTest) o;
return age == that.age &&
Objects.equals(name, that.name);
}
/**
* 重点是重写hashCode方法,返回一个固定的值
* @return
*/
@Override
public int hashCode() {
return 2;
}
@Override
public String toString() {
return "HashValTest{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
下面再说一下hashmap解决hash冲突的方式:
在数据结构中,处理hash冲常用的办法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而HashMap处理hash冲突的方法就是链地址法。
即当存在hash冲突时,以链表结构保存多个值,这种方法的基本思想就是将所有的哈希地址为i的元素构成一个称为同一次链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行,链地址法适用与经常进行插入和删除的情况。