您的位置:

从入门到精通:Java中的HashMap实现原理详解

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的使用方法和技巧。