HashMap的数据结构

目录

一、HashMap数据结构概述

二、详述以及具体代码

2.1 Node[ ] table数组

2.2 静态内部类Node

2.3 存放键值对的方式(关键)

2.4 HashMap的扩容

2.5 什么情况下发生扩容

2.6 每次扩容多大

2.7 最大容量为多少

2.8 扩容操作resize()源代码


一、HashMap数据结构概述

HashMap类是集合框架中Map接口的实现类,是一种键-值映射表,所以我们对比单列集合,它的优点就在于能够高效的通过key查找出对应的value。

  • 在JDK 1.7及之前,HashMap结构采用数组+ 单向链表的形式存储键值对。而且在链表中插入元素时,采用的是[头插法],即将新节点每次放在链表的头部。
  • 到了JDK 1.8以后其内部结构采用数组+单向链表+红黑树的形式存储键值对,在链表插入元素时改用[尾插法]。

 二、详述以及具体代码

 2.1 Node[ ] table数组

数组的类型为Node[ ],用于保存KV键值对,每个KV键值对都被封装成一个Node类型的对象,同时数组中的每个Node对象也是链表中的头结点。

public class HashMap{
    //每个Node对象保存一个KV键值对,同时也是链表中的头结点
    transient Node<K,V>[] table;

    //数组的默认容量为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    //最大容量为2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
}

2.2 静态内部类Node

public class HashMap{

    //静态内部类Node,为结点的类型
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //哈希值
        final K key; //键
        V value; //值
        Node<K,V> next; //下一个结点(只有next结点,所以该链表为单向链表)
        
        //构造方法
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        
        //hashcode方法,用key和value的hash值作异或运算
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

}

2.3 存放键值对的方式(关键)

前面介绍了基本的table数组,以及Node类的基本结构。那么如何把每一个Node存入table数组,这也是HashMap的精妙之处了,其存放过程为:

1. 当使用put()方法存放键值对时,首先调用hash()方法,计算出key的哈希值。该方法里将key的哈希值与其高16位进行了异或运算,这样算出的哈希值在计算下标时会更散列,减少了哈希冲突。

public class HashMap{
    //此方法的作用是将一个键值对存入HashMap对象中
    //调用了putVal()方法,putVal()方法又调用了hash(key)方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
}
static final int hash(Object key) {
     int h;
     // 通过key的hashCode()方法返回的哈希值与它的高16位进行异或运算
     // 作用:计算出的hash值,在计算下标位置时,会更“散列”,减少哈希冲突
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2. 得到key的哈希值后与数组的长度length进行&运算得到的值为Node对象在数组中的下标位置。

// 两种计算方式结果相同,但效率不同
int index1 = (数组长度 - 1) & hash值 // 位运算效率高,要求数组长度必须为2的n次幂
int index2 = hash值 % 数组长度 // 算术运算效率低

 得到下标后,将其对应的节点赋给临时节点p,如果为null则表示当前位置没有存入节点,那么就可以放在数组中,调用newNode()方法在该位置创建新的节点。

public class HashMap{

     // 添加键值对
     final V putVal(int hash, K key, V value) {
     Node<K,V>[] tab; // 临时数组
     Node<K,V> p; // 临时节点
     int n, i; // n代表数组长度,i代表元素下标位置

     // 根据当前元素的key的hash值,计算该元素在数组tab中的下标位置i
     if ((p = tab[i = (n - 1) & hash]) == null)
     tab[i] = newNode(hash, key, value, null);
     }
}

2.4 HashMap的扩容

HashMap在扩容的过程中需要按照数组容量加载因子(loadFactor)来进行判断。

数组容量: table[]数组的长度。如果没有指定容量,那么在添加第一个元素时,该数组按照默认值16进行初始化。

加载因子: 表示着HashMap集合中元素的填满程度,默认为0.75f,可以理解为百分比(个人看法)。该值越大表示该HashMap集合允许填满的元素就越多,对应的空间利用率就越高。但是由于元素的存储越来越密集,就会导致哈希冲突的概率增加。反之虽然降低了空间利用率,但是哈希冲突的概率就会降低。

public class HashMap{
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量为16
     static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子

     int threshold; // 扩容阈值
     final float loadFactor; // 加载因子
}

 2.5 什么情况下发生扩容

 HashMap的扩容方法是resize()方法。在两种情况下会发生扩容:

  • 情况1: 这里又会提到一个新的属性threshold(扩容阈值=数组容量*加载因子)。当HashMap集合中的元素个数超过这个阈值时,就会进行扩容。比如,默认情况下数组容量为16,加载因子为0.75f,那么阈值则为16*0.75=12,当元素个数超过12时数组就会扩容。
  • 情况2: 当添加新元素时,如果链表的长度大于8,就会将该条链表转换为红黑树结构。在这之前会有一步判断数组容量的操作。如果容量小于64,则会进行数组扩容;大于的话才会将链表转换为红黑树。由于链表的查找操作属于线性查找,效率比较低,所以会在长度大于8的时候转换为红黑树,以提高查找的效率。 
public class HashMap{
    /**
     * The next size value at which to resize (capacity * load factor).
     *
     *
     */
    int threshold;//扩容阈值(容量*加载因子)
}
// 添加新元素
final V putVal(int hash, K key, V value) {
     //...

     // 判断当前集合中的元素数量,是否超过阈值threshold
     if (++size > threshold)
     resize(); // 扩容
     //...
}
// 链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
     //...
     // 数组为空或者数组的长度n小于64
     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
     resize(); // 扩容
     // ...
}

当链表长度大于8时,将其转换为红黑树。

public clas HashMap{
    // 默认链表长度
    static final int TREEIFY_THRESHOLD = 8;

    // 判断链表数量是否大于8
    if (binCount >= TREEIFY_THRESHOLD - 1)
        // 链表长度超过8,将当前链表转换为红黑树
        treeifyBin(tab, hash);
}

转换为红黑树的源代码:

public class HashMap{
    // 转换为红黑树
    final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 数组长度如果小于64,则优先扩容数组
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
         resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
         // 遍历链表节点,转换为红黑树
         TreeNode<K,V> hd = null, tl = null;
         do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
             }
         tl = p;
         } while ((e = e.next) != null);
         if ((tab[index] = hd) != null)
             hd.treeify(tab);
         }
     }
}

2.6 每次扩容多大

HashMap在扩容时会按照当前数组容量的2倍进行扩容。

// 新容量 = 原有容量的2倍
newCap = oldCap << 1;

2.7 最大容量为多少

在每次扩容时都会检查当前容量是否超出常量值MAXIMUM_CAPACITY,如果超出则不会扩容。常量值 MAXIMUM_CAPACITY为1<<30 ,计算结果为 1073741824。所以, HashMap 集合数组的最大容量为 1073741824 。

static final int MAXIMUM_CAPACITY = 1 << 30;

2.8 扩容操作resize()源代码

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倍)
         // 新容量没有超出最大容量,并且原容量大于等于默认初始容量16
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
             newThr = oldThr << 1; // 设置新扩容阈值(同样为原扩容阈值的2倍)
     }
     else if (oldThr > 0) // 新容量按照初始化(构造方法)中的扩容阈值设置
         newCap = oldThr;
     else { // 默认
             newCap = DEFAULT_INITIAL_CAPACITY; // 默认初始化容量16
             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认扩展阈值 = 默认加载因子0.75 * 默认初始化容量16
     }

     // 如果新扩容阈值等于0,则需要重新计算,最大不超过Integer.MAX_VALUE
     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) // 当前元素为红黑树节点,将当前红黑树拆分为2棵红黑树
         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
     else { // 当前元素为链表节点,将当前链表拆分为两条链表(高位链表、低位链表)
         Node<K,V> loHead = null, loTail = null;
         Node<K,V> hiHead = null, hiTail = null;
         Node<K,V> next;
     // 遍历链表,根据每个节点的hash值,重新计算链表位置
    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集合的数据结构的说明,请大家多多参考,不完善的地方请提供更好的意见,感谢阅读! ! !

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值