HashMap常用方法及底层原理

一、什么是HashMap

    HashMap 是 Java 中的一个关键数据结构,属于 java.util 包。它基于哈希表的实现,提供了快速查找、插入和删除操作。以下是 HashMap 的一些主要特性:

  1. 键值对存储:HashMap 存储键值对(key-value pairs),其中键(key)是唯一的。

  2. 非同步:HashMap 不是线程安全的。在多线程环境中,如果多个线程同时修改 HashMap,而没有适当的同步措施,可能会导致不可预知的行为。

  3. 允许空键和空值:HashMap 允许键或值为 null。

  4. 非有序:HashMap 不保证元素的顺序,特别是它不保证该顺序恒久不变。

  5. 初始容量和负载因子:可以通过构造函数设置 HashMap 的初始容量和负载因子。初始容量是哈希表中桶的数量,负载因子是一个影响哈希表性能的参数,它定义了哈希表在其容量自动增加之前可以达到多满。

  6. 哈希冲突解决:当两个对象具有相同的哈希码时,会发生哈希冲突。HashMap 使用链表(在 Java 8 之前)或链表和红黑树(Java 8 及之后)来解决冲突。

  7. 性能:HashMap 提供了常数时间的性能(即 O(1) 时间复杂度)对于 get 和 put 操作,假设哈希函数良好且哈希表没有过载。

  8. 迭代器:HashMap 提供了键集(key set)、值集(values)和键值对集(entry set)的视图,它们都可以被迭代。

  9. 默认构造方法:如果使用无参构造方法创建 HashMap,它会使用默认的初始容量(16)和默认的负载因子(0.75)。

主要特性总结key-value形式的键值对;无序不重复;线程不安全

    

二、HashMap的链表与红黑树

1、数据结构

HashMap是一种存取高效但不保证有序的集合,它的数据结构在1.7里面是 数组+链表在1.8之后是数组+链表+红黑树

在这里插入图片描述
    
默认初始容量16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

将链表转为红黑树的阈值

 static final int TREEIFY_THRESHOLD = 8;

将红黑树转化为链表的阈值

 static final int UNTREEIFY_THRESHOLD = 6;

将链表转化为红黑树时,数组的大小必须大于等于这个值。否则如果 TREEIFY_THRESHOLD 大于8,将扩容,而不是转为红黑树

static final int MIN_TREEIFY_CAPACITY = 64;

    

2、链表转为红黑树

在 Java 8 及以后的版本中,HashMap 在某些情况下会将链表转换为红黑树,以提高搜索效率。这种转换发生在以下两个条件同时满足时:

  1. 树化阈值:HashMap 有一个树化阈值(treeify threshold),当链表的长度超过这个值时,链表会被转换为红黑树。在 Java 8 中,这个值默认是 8。

  2. 桶(bucket)的数量:如果桶的数量小于 64,即使链表长度达到 8,也不会立即转换为红黑树。这是为了避免在哈希表较小时过度优化,因为小规模的哈希表中,链表的性能已经足够好。

    
转换过程
    当 HashMap 进行扩容时,如果桶的数量增加到 64 或更多,那么在重新计算哈希值并重新插入元素的过程中,任何长度大于等于 8 的链表都会被转换为红黑树。这个转换是在扩容过程中自动完成的。

    
为什么有这个限制

  • 性能考虑:在哈希表较小时,链表的性能通常已经足够好,不需要额外的复杂性来维护红黑树。
  • 内存考虑:红黑树比链表占用更多的内存,因此在小规模的哈希表中使用链表可以节省内存。

    在实际应用中,这意味着如果你的 HashMap 初始容量设置得较小,或者在插入数据时没有触发扩容,那么即使某些链表的长度超过了 8,它们也可能不会立即转换为红黑树。只有当哈希表的容量增加到一定大小,且链表长度满足条件时,才会进行转换
    

3、红黑树退化为链表

     当一个桶中的红黑树的节点数量减少到一定阈值以下时,HashMap 会将这个红黑树转换回链表。这个阈值默认是 6。这意味着,如果一个桶中的红黑树的节点数量降到 6 个或更少,那么这个红黑树会被转换回链表。
    
为什么需要转换

  • 性能优化:链表在节点数量较少时,其操作(如搜索、插入、删除)的性能通常优于红黑树,因为链表的结构更简单,没有红黑树的复杂性。
  • 内存使用:红黑树比链表占用更多的内存,因为它需要存储额外的指针(用于维护树的结构)。当节点数量较少时,使用链表可以减少内存的使用。

    
转换过程

转换过程通常发生在以下情况下:

 1. 删除操作:当从 HashMap 中删除元素时,如果某个桶中的红黑树节点数量减少到 6 个或更少,这个红黑树会被转换回链表。
 2. 扩容操作:在 HashMap 进行扩容时,如果桶的数量增加,那么在重新计算哈希值并重新插入元素的过程中,如果某个桶中的红黑树节点数量减少到 6 个或更少,这个红黑树会被转换回链表。

    

三、存储(put)操作

    
    jdk1.7采用的是头插法 ,因头插法在扩容时导致的死循环,jdk1.8中链表插入采用的是尾插法。

put操作的步骤如下:

  1. 待存储的key进行hash计算
  2. 如果hash数组为空或者数组长度为0,则进行初始化
  3. 数组根据key计算后的hash值进行索引数组元素:
    • 如果索引出来的元素为空,则新创建一个Node节点并添加到数组中

    • 如果索引出来的元素不为空,则比较索引出来元素的key与传入的key进行"=="或equals的比较

      ① 如果一致则用新的value值替换旧的value值
      ②如果不一致就判断索引出来节点是不是树形节点,如果是则按照树形节点进行更新
      ③如果不是树形节点,则判断索引出来的节点的next节点是否为空,如果下一个节点为空,则新建下一个节点,此时需要判断链表的长度是否大于等于8,如果等于8,则要进行树化转为红黑树。
      ④ 如果下一个节点不为空,就一直循环找到节点的hash值与传入key的hash值一致,key通过"=="或equals比较是一致的。然后用新的value值替换旧的value值

   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为空,则初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //key对应的桶不存在,则创建一个新节点并添加到桶中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        //key对应的桶存在,则遍历桶,找到key对应的节点,如果key相同,则更新value,否则创建一个新节点
        else {
            Node<K,V> 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<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //节点key不相同也不是树形节点
            else {
                for (int binCount = 0; ; ++binCount) {
                    //节点的next节点为空,则新建节点并判断是否需要树化
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                   	// 节点的next节点不为空,且key相同
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            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;
    }

    

四、读取(get)操作

    
获取数据步骤如下:

  1. 如果数组不为空,则根据hash值找到对应的桶,对应的桶为空,就返回null
  2. 对应的桶不为空,如果桶的第一个元素的key和key相等,则返回该元素
  3. 如果桶的第一个元素的key和key不相等
    • 如果桶的第一个元素的next节点不是树节点,则遍历桶,找到key对应的节点,如果key相同,则返回该节点
    • 如果桶的第一个元素是树节点,则调用树节点的getTreeNode方法
 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
    
    Node<K,V>[] tab;
    Node<K,V> first, e; 
    int n; 
    K k;
    
    // 如果数组不为空,则根据hash值找到对应的桶,对应的桶也不为空
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        
        // 如果桶的第一个元素的key和key相等,则返回该元素
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        // 如果桶的第一个元素的next节点不是树节点,则遍历桶,找到key对应的节点,如果key相同,则返回该节点
        if ((e = first.next) != null) {
            // 如果桶的第一个元素是树节点,则调用树节点的getTreeNode方法
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

    

五、扩容(resize)操作

    
函数resize()用于实现哈希表的扩容操作。具体步骤如下:

  1. 获取旧表信息

    • 获取当前哈希表table的引用。
    • 获取当前哈希表的容量oldCap。
    • 获取当前哈希表的阈值oldThr。

        

  2. 判断是否需要扩容:

    • 如果当前容量大于等于最大容量MAXIMUM_CAPACITY,设置阈值为整型最大值并返回旧表。
    • 如果当前容量大于0且小于最大容量,计算新容量newCap为旧容量的两倍,并更新阈值newThr为旧阈值的两倍。
    • 如果当前容量为0但阈值大于0,将新容量设为阈值。
    • 如果当前容量和阈值都为0,使用默认初始容量DEFAULT_INITIAL_CAPACITY并计算阈值。
          
  3. 计算新阈值

    • 如果新阈值仍为0,根据负载因子计算新阈值。
          
  4. 创建新表

    • 更新阈值threshold为newThr。
    • 创建新的哈希表newTab,大小为newCap。
          
  5. 迁移旧表数据:

    • 遍历旧表oldTab中的每个桶。
    • 对于非空的桶,将其元素迁移到新表中。
    • 如果桶中的元素只有一个,直接插入新表对应位置。
    • 如果桶中的元素是红黑树节点,调用split方法进行拆分。
    • 如果桶中的元素是链表,遍历链表并根据哈希值分成两部分,分别插入新表的不同位置。
 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;

        // 计算新的容量和阈值
        if (oldCap > 0) {
            // 如果容量大于最大容量,则将阈值设置为最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } // 如果新容量等于旧容量的2倍且小于最大容量且旧的容量大于等于默认容量,新阈值设置为旧的容量的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold 新的阈值为旧的阈值的2倍
        }
        // 如果当前容量为0但阈值大于0,将阈值设为新容量。
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 如果当前容量和阈值都为0,使用默认初始容量DEFAULT_INITIAL_CAPACITY并计算阈值。
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        //  如果新阈值仍为0,根据负载因子计算新阈值。
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
         //创建新的哈希表newTab,大小为newCap。
        Node<K,V>[] newTab = (Node<K,V>[])new HashMap.Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof HashMap.TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    

六、HashMap的线程安全与顺序

1、线程安全

HashMap 是非线程安全的,这意味着多个线程同时修改 HashMap 可能会导致不可预知的行为。如果需要在多线程环境中使用 HashMap,有几种方法可以使其线程安全:

  1. 使用 Collections.synchronizedMap 方法

    Java 提供了一个 Collections.synchronizedMap 方法,可以将 HashMap 包装为线程安全的 Map。

    Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>());
    
  2. 使用 ConcurrentHashMap

    ConcurrentHashMap 是 HashMap 的线程安全版本,它提供了更好的并发性能。通常,它是实现线程安全的首选方式,因为它允许多个线程同时读写而不需要外部同步。

    Map<K, V> map = new ConcurrentHashMap<K, V>();
    
  3. 使用 synchronized 块或方法

    在访问 HashMap 的方法上使用 synchronized 关键字,确保在修改 HashMap 时只有一个线程可以执行。

    public void put(K key, V value) {
        synchronized (this) {
            this.map.put(key, value);
        }
    }
    

        

2、有序的Map

     HashMap 本身不保证有序,即它不保证元素的顺序,无论是插入顺序还是自然顺序。如果需要一个有序的映射,你可以使用以下几种替代方案:

  1. LinkedHashMap:

    LinkedHashMap 是 HashMap 的一个子类,它维护了元素的插入顺序或者访问顺序。如果你想要元素按照插入顺序存储,可以使用 LinkedHashMap

  2. TreeMap:

    TreeMap 是一个基于红黑树实现的 NavigableMap 接口的类,它可以按照元素的自然顺序或者自定义比较器(Comparator)定义的顺序来存储元素。TreeMap 保证了元素的有序性,并且提供了一些导航方法,如 firstEntry()、lastEntry()、higherEntry() 等。

    Map<K, V> map = new TreeMap<K, V>();
    

    或者,如果你想要自定义排序,可以提供一个比较器:

    Comparator<K> comparator = ...;
    Map<K, V> map = new TreeMap<K, V>(comparator);
    
  3. 自定义有序 HashMap:

    如果你需要 HashMap 的某些特性,并且想要保持顺序,你可以自己实现一个有序的 HashMap。这通常涉及到在内部维护一个列表来跟踪插入顺序。
        

  4. Hashtable:

    虽然 Hashtable 是一个遗留类,但它是同步的,并且它按照插入顺序维护键的顺序。然而,由于 Hashtable 的性能通常不如 HashMap 和 LinkedHashMap,并且它不是线程安全的,所以通常不推荐使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值