JDK8 源码之HashMap(1)

本文详细解析了HashMap的实现原理,包括其内部结构、扩容机制以及与红黑树的转换过程。介绍了HashMap如何通过负载因子和阈值控制容量,以及链表与红黑树之间的自动转换,以提高查找效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap是什么

源码出发:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {...}

可以看到HashMap实现了Map、Cloneable、Serializable接口,实现了AbstractMap抽象类。
Map(附录有详细介绍)为一个将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
HashMap在jdk1.8中,实现上有了一个很大的优化,实现方式有原来的数组加链表,变成了数组、链表和红黑树。在性能上有了一个较大的提升,也一定程度上解决了hash值碰撞带来的性能损失。

HashMap-Fields

再继续看看Fields:

//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//当table容量大于该值,threshold设置成为固定Integer.MAX_VALUE。
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表的节点数大于阈值时,转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

//Node节点数组(hash表)
transient Node<K,V>[] table;
//Map.Entry Set对象,用于遍历
transient Set<Map.Entry<K,V>> entrySet;

//存储元素数目
transient int size;
//结构变更次数
transient int modCount;
//下次调整大小的临界值(table大小*负载因子)
int threshold;
//哈希表的负载因子。
final float loadFactor;

可以看到,hashMap存在一个负载因子,当hashMap容量超过threshold(负载因子*数组长度)会发生扩容。链表与红黑树的相互转化各存在一个阈值,同时也使用modCount来记录‘结构变更’用以保证fail-fast。

HashMap-Node

Node节点还是一览无余:

 static class Node<K,V> implements Map.Entry<K,V> {
         //hash值
        final int hash;
        final K key;
        V value;
        //指向下个结点
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        //获取节点hash值
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //设置节点值
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //判断节点是否相同:节点值的key且value调用equals为true
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

node节点为链表节点,除了记录当前的key、value值及next节点时,也将key的hash值记录来,杜绝重复计算带来的性能损失。

HashMap-TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        ....
}

当链表的长度超过默认阈值时,该链表会转化成红黑树,防止链表过长,造成查找性能缓慢,也是jdk8中一个比较重要的优化。关于TreeNode的实现和操作会在后面单独开一篇博客来说明,有兴趣的可以看看

HashMap构造方法
 //默认传参数
 public HashMap() {
      //负载因子默认0.75
      this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

 //指定初始容量
 public HashMap(int initialCapacity) {
    //负载因子默认为0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//指定初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //初始容量最多为MAXIMUM_CAPACITY                                     
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    //计算得到下载扩容容量的临界值(返回值都为2幂次方-1)
    //可以看下面的解释,这里threshold的值会实际是table的初始容量
    this.threshold = tableSizeFor(initialCapacity);
    }

   //设置初始值
   public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        //该方法后面再做介绍
        putMapEntries(m, false);
    }

可以看到的,hashMap初始化未添加元素的时,并未实例化table,只是记录来容量和负载因子。而是在第一次添加的操作的时候,才会实例化table。
而且可以看出设置initialCapacity时,并没有按照你给的initialCapacity取初始化table容量,而是将tableSizeFor(initialCapacity)的返回值newCap赋值给threshold,结合后面的resize方法,初始化时会走‘分支二’,table的容量会被初始化成newCap大小。

HashMap-resize扩容

接下来,看看hashmap怎么扩容的:

 final Node<K,V>[] resize() {
    //获取当前table对象
    Node<K,V>[] oldTab = table;
    //获取当前容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取扩容临界值
    int oldThr = threshold;
    //声明新容量、新负载因子
    int newCap, newThr = 0;
    if (oldCap > 0) {//“分之一”
    //当前容量大于0时
        if (oldCap >= MAXIMUM_CAPACITY) {
            //当前容量大于于MAXIMUM_CAPACITY
            //直接设置threshold的值为Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //新容量newCap=当前容量*2 
            //新临界点threshold=当前临界点threshold*2
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) //“分之二”
    //当前容量等于0,但threshold已经指定,则容量=threshold
    //例如:HashMap(initCap,0.75)初始化时,oldCap=0,threshold>0
        newCap = oldThr;
    else {   //“分之三”
        //当前容量等于0时(例如第一次添加元素时)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //走“分之二”,newThr==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"})
        Node<K,V>[] newTab = (Node<K,V>[])new 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 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 {
                         //记录在新table中位置需要变动的
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                   //这一段的算法应该来说比较巧妙哦,
                   //已知:新扩容=当前容量*2
                   //假设当前容量为16(二进制:10000),
                   //假设元素hash值后5位为(a:10010,b:00010)
                   //a和b元素位置一致(cap-1)&hash为2位(00010)
                   //新容量:32(100000)
                   //新位置 a:第18位(10010),b第2位(00010)
                   //可以看出,a和b元素的位置正好相差oldCap
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
    }

resize方法注释已经很详细了,主要有一个点妙传就是扩容时,每个链表中原有元素的位置要么不动,要么移动oldCap个位置。而且插入顺序不变,jdk1.7中则会反序。

其他操作

限于篇幅,可以看后面的博客

附录 Map接口

Map 方法说明
public interface Map<K,V> {
  //map大小
  int size();
  //是否为空
  boolean isEmpty();
   //map是否含有当前key
  boolean containsKey(Object key);
  //map是否含有当前value
  boolean containsValue(Object value);
  V get(Object key);
  V put(K key, V value);
  V remove(Object key);
  //批量设置
  void putAll(Map<? extends K, ? extends V> m);
  void clear();
  //获取key Set列表
  Set<K> keySet();
  //获取value Set列表
  Collection<V> values();
  //获取遍历对象列表
  Set<Map.Entry<K, V>> entrySet();
  //遍历节点接口定义
  interface Entry<K,V> {....}
  boolean equals(Object o);
  int hashCode();
  }

可以看到Map接口定义了一个map必须实现的操作,增加、插入、修改、遍历等基本方法,同时定义了内部遍历节点Entry接口。不过在jdk8之后,可以看到Map接口中多了很多default方法,虽然都不复杂,但是可以借鉴一下这种思想,还是很有好处的。

Map default方法

大概浏览下,默认方法如:getOrDefault、forEach等方法实现都比较简单,就不描述细节了,只是大概说一下是什么功能了。

//通过键值key获取值为null时,则返回defaultValue
default V getOrDefault(Object key, V defaultValue){...}

//遍历键值对,执行指定动作
default void forEach(BiConsumer<? super K, ? super V> action){...}

//遍历键值对,对每个键值对执行指定动作后的返回值覆盖当前值
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function){...}

//当前键值key获取值为null时,则设置值为value
default V putIfAbsent(K key, V value) {}

//删除键值为key且值为value的键值对
//map已经存在键值key
default boolean remove(Object key, Object value){}

//覆盖键值对(键值为key且值为oldValue)的值为newValue,否则不覆盖
//map已经存在键值key
default boolean replace(K key, V oldValue, V newValue){}

//设置map中键值为key的的值为value
//map已经存在键值key
default V replace(K key, V value){}

//当get(key)为空时,则mappingFunction.apply(key)计算得到的新值newValue不为空时,则put(key,newValue)并返回newValue,否则返回null
default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {}

//与computeIfAbsent基本相同,新增了一个当get(key)为空时,remove(key)
default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {}

//mappingFunction.apply(key)计算得到的新值newValue
//不为空时,put(key,newValue) 返回newValue
//为空时,remove(key) 返回null
default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {}

//1.mappingFunction.apply(key)计算得到的新值newValue
//2.get(key)为null时,put(key,value),否则 put(key,newValue)
//3.remove(key)
default V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction){}

可以看到接口中的默认方法还是很丰富,而且都比较简单,但阅读过程中,这些接口的定义,方法的抽象,对于一个程序猿来说还是能学到很多东西。

Map-Entry default方法
interface Entry<K,V> {
 public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
        return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

 public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
        return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }
  public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
         return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }
 public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
         return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }

}
<think>嗯,用户让我介绍一下JDK8HashMap源码。首先,我得先回想一下HashMap的基本结构。在JDK8之前,HashMap是数组加链表的结构,当哈希冲突较多时,链表会变得很长,查询效率会下降。而JDK8之后,当链表长度超过8时,会转为红黑树,这样查询的时间复杂度就从O(n)变成O(log n)了。这个优化应该是个重点。 接下来,我需要考虑HashMap的几个核心部分:数据结构、哈希计算、put方法、扩容机制、红黑树转换条件等。用户可能对这些部分感兴趣,特别是和之前版本不同的地方,比如红黑树的引入。 然后,数据结构方面,HashMap内部有一个Node数组,每个Node可能是一个链表节点或者树节点。需要解释一下Node类的结构,包括hash、key、value、next这些属性。当链表转成树时,会用TreeNode,这可能继承自LinkedHashMap的Entry,但具体的继承关系可能需要查证一下。 哈希计算方面,JDK8的hash方法做了优化,将高位的信息也参与到索引的计算中,减少哈希冲突。具体来说,hashCode()的高16位和低16位进行异或,这样在数组长度较小时,高位的变化也能影响最终的索引位置,减少碰撞。 put方法的流程是关键。当插入一个键值对时,首先计算哈希值,找到数组下标。如果该位置为空,直接插入;如果是链表或树节点,则遍历查找是否存在相同的key,存在则替换,否则添加到链表末尾或树中。当链表长度达到8且数组长度大于等于64时,链表转红黑树,否则进行扩容。这个阈值需要注意,可能用户会问到为什么是8或者64。 扩容机制,也就是resize方法。当元素数量超过容量乘以负载因子时,数组会扩容为原来的两倍。扩容后,原来的节点会重新计算位置,要么在原位置,要么原位置+旧容量。这里可能涉及到高位判断,通过(e.hash & oldCap)是否为0来决定位置。同时,在JDK8中,链表在扩容时会被拆分成两个链表,保持顺序,避免死循环的问题,而JDK7之前可能因为头插法导致死循环。 线程安全方面,HashMap不是线程安全的,多线程环境下可能出现问题,比如数据覆盖、死循环(在JDK7中)等。需要提到ConcurrentHashMap或者使用Collections.synchronizedMap来保证线程安全。 另外,初始容量和负载因子的默认值,初始容量是16,负载因子0.75,这些参数的作用和调整的影响也需要解释一下。比如负载因子高可以减少空间开销,但增加查找成本;负载因子低则相反。 红黑树转换的条件需要明确,链表长度超过8且数组长度至少64才会转换,否则只是扩容。这是为了避免在哈希表还很小的时候,不必要的树化,因为扩容可能更有效。 可能用户还想知道源码中的一些具体实现细节,比如TreeNode的结构,红黑树的插入、删除操作是如何实现的,但可能这部分太深入,需要根据用户的背景决定是否展开。 在解释的时候,需要注意避免过于技术化的术语,用比较易懂的语言,同时结合代码结构,比如putVal方法、resize方法的关键步骤。可能还需要提到modCount用于快速失败机制,迭代器在检测到结构变化时抛出异常。 用户可能的疑问点包括:为什么选择红黑树而不是其他数据结构?为什么阈值是8?扩容为什么是两倍?这些可能需要准备答案,比如红黑树在查询和插入之间取得平衡,阈值8是基于泊松分布统计,两倍扩容是为了保持容量为2的幂,方便位运算计算索引。 总结下来,应该按照数据结构、哈希计算、put流程、扩容机制、树化条件、线程安全等方面来组织回答,重点突出JDK8的改进,比如红黑树的引入和扩容优化。</think>在 JDK8 中,`HashMap` 是一个基于哈希表实现的键值对容器,其源码设计在性能和数据结构上进行了优化(如引入红黑树)。以下是对其核心实现的介绍: --- ### **1. 数据结构** JDK8 的 `HashMap` 使用 **数组 + 链表 + 红黑树** 的复合结构: - **数组(`Node<K,V>[] table`)**:存储链表的头节点或红黑树的根节点。 - **链表(Node)**:解决哈希冲突的基础结构,每个节点存储键值对和下一个节点的引用。 - **红黑树(TreeNode)**:当链表长度超过阈值(默认 8)且数组长度 ≥ 64 时,链表转换为红黑树,提升查询效率。 ```java static class Node<K,V> implements Map.Entry<K,V> { final int hash; // 哈希值 final K key; V value; Node<K,V> next; // 链表的下一个节点 } ``` -- ### **2. 哈希计算** 通过 `hash()` 方法将键的哈希码分散到数组中,减少碰撞: ```java static final int hash(Object key) { int h; // 高16位与低16位异或,增加低位的随机性 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` -- ### **3. `put()` 方法流程** 1. **计算哈希值**:根据 `key` 计算哈希值,确定数组下标 `(n-1) & hash`。 2. **处理空桶**:若目标位置为空,直接插入新节点。 3. **处理冲突**: - **链表**:遍历链表,若 `key` 已存在则更新值,否则添加到链表尾部。 - **红黑树**:调用树的插入方法。 4. **树化检查**:链表长度 ≥ 8 时尝试树化(需数组长度 ≥ 64,否则优先扩容)。 5. **扩容检查**:元素总数超过 `容量 * 负载因子`(默认 0.75)时触发扩容。 --- ### **4. 扩容机制(`resize()`)** - **新容量**:原容量的 2 倍(保持为 2 的幂)。 - **数据迁移**: - 链表节点根据 `(e.hash & oldCap)` 是否为 0 分配到原位置或新位置。 - 红黑树节点会拆分为两个链表,若长度 ≤ 6 则退化为链表。 -- ### **5. 红黑树转换条件** - **链表 → 红黑树**:链表长度 ≥ 8 **且** 数组长度 ≥ 64。 - **红黑树 → 链表**:树节点数 ≤ 6 时退化为链表。 --- ### **6. 关键参数** - **默认初始容量**:16。 - **负载因子(`loadFactor`)**:默认 0.75(平衡空间与时间效率)。 - **扩容阈值**:`容量 * 负载因子`。 - **树化阈值**:链表长度 ≥ 8,数组长度 ≥ 64。 --- ### **7. 线程安全性** `HashMap` **非线程安全**,多线程操作可能导致数据不一致。替代方案: - 使用 `ConcurrentHashMap`。 - 通过 `Collections.synchronizedMap()` 包装。 -- ### **8. 性能优化点** - **红黑树**:避免长链表的低效查询(时间复杂度从 O(n) 优化到 O(log n))。 - **高位异或哈希**:减少哈希碰撞。 - **扩容优化**:避免 JDK7 及之前版本的头插法导致的死循环问题。 -- ### **源码分析示例(简化的 `put()` 流程)** ```java 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; // 初始化或扩容 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 { // 处理哈希冲突... } // 检查扩容... } ``` --- 通过以上设计,JDK8 的 `HashMap` 在高并发和高负载场景下显著提升了性能,同时保持了合理的空间利用率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值