HashMap是Java语言中最常用的的数据结构之一,它的高效性和易用性使它成为程序员们必备的工具之一。本文将从HashMap的原理入手,详细阐述它的实现细节和注意事项,帮助读者掌握HashMap的使用方法和技巧。
一、什么是HashMap
HashMap是Java语言中的一种散列表,它可以存储键值对的映射关系。HashMap可以快速地进行插入、查找和删除操作,时间复杂度均为O(1)。HashMap底层实现是通过数组和链表相结合的方式来实现散列表。当链表长度大于阈值(默认为8)时,链表会转化成红黑树,以保证插入、查找和删除操作的平均时间复杂度为O(1)。
二、HashMap的构造方法
以下是HashMap的几种构造方法:
1. HashMap() : 创建一个空的HashMap 2. HashMap(int initialCapacity) : 创建指定大小的HashMap 3. HashMap(int initialCapacity, float loadFactor) : 创建指定大小和加载因子的HashMap 4. HashMap(Map m) : 创建指定Map的HashMap
其中,initialCapacity是HashMap的初始容量,loadFactor是负载因子,如果负载因子为0.75,则当HashMap的大小达到容量的75%时,会自动增加容量。如果初始容量和负载因子没有给出,则默认初始容量为16,负载因子为0.75。
三、HashMap的put()方法
以下是HashMap的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[] tab; Node p; int n, i;
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 e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)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)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put()方法是非常重要的方法,它将键值对插入到HashMap中。它的实现细节如下:
1. 首先,通过hash()方法计算键的哈希值,判断table是否为空或长度为0。如果是,通过resize()方法进行初始化,否则直接获取table的长度n。
2. 然后,通过(n – 1) & hash计算出要插入的位置,p表示该位置的第一个节点。如果该位置为空,则直接插入键值对,否则执行第3步。
3. 判断该位置上的第一个节点的key是否和要插入的key相等,如果相等则执行覆盖操作。如果不相等,判断该位置上的第一个节点是否为TreeNode类型。如果是,则执行红黑树的put操作,否则遍历该链表,找到最后一个节点,插入新的节点后,如果该链表的长度大于等于8,则通过treeifyBin方法将该链表转化成红黑树。
4. 插入完成以后,判断size是否大于threshold,如果是,则通过resize()方法进行扩容。
四、HashMap的get()方法
以下是HashMap的get()方法:
public V get(Object key) {
Node e;
// 通过hash方法计算hash值,然后在table数组中寻找key所在的节点
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
// table不为空,取得table数组的长度n,n>0
if ((tab = table) != null && (n = tab.length) > 0 &&
// 通过(n - 1) & hash获取key在table数组中的位置first
(first = tab[(n - 1) & hash]) != null) {
// 如果在table的first位置上就找到相同的key,则返回这个节点first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果first位置没有找到相同的key,则处理next链表
if ((e = first.next) != null) {
// 如果first位置是TreeNode节点,则使用TreeNode的方式获取相同的key
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
// 否则,遍历next链表查找相同的key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 如果table为空或key没有匹配到返回null
}
get()方法是HashMap中的另一个重要方法,它以键为参数,返回与之关联的值。get()方法的实现细节如下:
1. 首先通过getNode()方法获取到key所在的节点。getNode()方法也是一个非常重要的方法,它遍历了HashMap中的所有元素,寻找与key相同的元素,如果找到,则返回与之对应的节点,否则返回null。
2. 如果key所在的节点不为空,则返回该节点对应的值,否则返回null。
五、HashMap的remove()方法
以下是HashMap的remove()方法:
public V remove(Object key) {
Node e;
// 通过hash方法计算hash值,然后在table数组中寻找key所在的节点
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
// 如果table不为空,且key在table中存在,则执行删除操作
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v;
// 判断p节点是不是要被删除的节点,并找到p链表上的最后一个节点node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到要被删除的节点,则执行删除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
remove()方法是HashMap删除元素的方法,它需要传入一个key作为参数,并返回被删除的元素的value值。remove()方法的实现细节如下:
1. 通过hash()方法计算键的哈希值,然后获取该键所在的节点p,并获取在该节点上的最后一个节点node。如果从p节点开始遍历整个链表都没有找到与key相同的节点,则返回null。
2. 如果找到了与key相同的节点,首先判断该节点是否是TreeNode类型。如果是,则使用TreeNode的方式删除节点。否则,判断要被删除的节点是否是p节点。如果是,则直接将p节点的下一个节点赋给tab[index]。否则,通过遍历链表,将要被删除的节点删除掉。
3. 执行完删除操作以后,判断size是否小于threshold的0.25,如果是,则通过resize()方法进行缩容。
六、HashMap的注意事项
1. HashMap的线程安全性问题
HashMap是非线程安全的,如果多个线程同时对一个HashMap进行读写操作,可能会出现数据不一致的情况。为了解决这个问题,可以使用线程安全的ConcurrentHashMap。
2. HashMap中键对象的注意事项
为了保证HashMap数据的一致性,键对象必须实现equals()方法和hashCode()方法,这两个方法的实现决定了HashMap的查找、插入和删除操作的正确性和效率。在使用HashMap时,需要特别注意键对象的实现细节,尽量避免相同的键对象产生哈希碰撞的情况,从而提高HashMap的效率。
3. HashMap的默认容量和负载因子
HashMap的默认容量为16,负载因子为0.75。如果不特别指定初始容量和负载因子,则采用默认值。在实际应用中,建议根据数据量大小和访问频率来选择合适的初始容量和负载因子,以提高HashMap的效率和性能。
七、总结
本文对HashMap的实现原理进行了详细的阐述,包括构造方法、put()方法、get()方法、remove()方法等。同时,我们也介绍了HashMap的注意事项,如线程安全性问题、键对象的实现细节、默认容量和负载因子等。希望通过本文的学习,能够帮助读者更好地掌握HashMap的使用方法和技巧。
原创文章,作者:小蓝,如若转载,请注明出处:https://www.506064.com/n/233965.html
微信扫一扫
支付宝扫一扫