HashMap是Java语言中最常用的的数据结构之一,它的高效性和易用性使它成为程序员们必备的工具之一。本文将从HashMap的原理入手,详细阐述它的实现细节和注意事项,帮助读者掌握HashMap的使用方法和技巧。
一、什么是HashMap
HashMap是Java语言中的一种散列表,它可以存储
二、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) { Nodee; // 通过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) { Nodee; // 通过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的使用方法和技巧。