深入理解HashMap(源码解析)

1. HashMap概述

  • key-value 允许使用null

  • 线程不安全

  • value构成的集合是Collection:无序的、可以重复的。所以value所在的类要重写:equals()
    所以HashMap 判断两个 value相等的标准:两个 value 通过 equals() 方法返回 true

  • key构成的集合是Set:无序的、不可重复的。所以key所在的类要重写 equals()、hashCode()方法
    所以 HashMap 判断两个 key 相等的标准:两个 key 的hashCode 值相等且通过 equals() 方法返回 true

  • 无序性:不等同于随机性。只是添加数据的顺序不是按照索引递增的,而是根据hash值计算得到的
    不可重复性:调用hashcode( )方法和equals( )方法判断。(如果存放在key中对象的实现类没有重写hashcode( )和equals( )方法,则会调用Object类中的hashcode( )和equals( )方法。通过JDK源码发现Object类中的hashcode( )方法是native 的,所以未重写以上2个方法或只重写的equals()方法时,可能可以向key中添加两个相同的对象 )

//Person类没有重写hashcode()、equals()方法
HashMap hashMap=new HashMap();
hashMap.put(new Person("Tom", 20), "A");
hashMap.put(new Person("Tom", 20), "B");
Iterator iterator = hashMap.entrySet().iterator();
while(iterator.hasNext()) 
{
	  Map.Entry entry = (Map.Entry)iterator.next();
      System.out.println(entry.getKey() +" - "+entry.getValue());
}
//Person [name=Tom, No=20] - A
//Person [name=Tom, No=20] - B

通过debug发现,add()时调用了Object.hashcode()方法,没有调用Object.equals()。
因为 Object.hashcode()为两个Person对象随机分配的hashcode值,以至于直接添加到Set集合中而没有调用equals()
  • IDE自动重写hashcode()方法
//Person类(String name  int age)重写hashcode()
public int hashCode()
{
	final int prime = 31;
	int result = 1;
	result = prime * result + No;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}
  • 为何重写hashCode( )方法时有31这个数字?
  • 尽量选择大系数。大系数时计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
  • 31只占用5bits,相乘造成数据溢出的概率较小。
  • 31可以由i*31== (i<<5)-1来表示。计算机底层的乘除法器都是用基本的加法器(减法器构成的),所以用移位运算符和减法代替乘法能加快运行速度
  • 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身、被乘数、1来整除(减少冲突)

2. 源码解析(JDK7)

2.1 概述

  • key-value 构成Entry数组
  • 底层存储结构:数组+链表
  • 添加元素时链表指向:新元素指向旧元素
    在这里插入图片描述

2.2 添加元素过程

在这里插入图片描述

2.3 源码分析

1.变量  
static final int DEFAULT_INITIAL_CAPACITY = 16; //默认容量
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子
transient int size;//HashMap中已经存储的键值对的数量
int threshold; //扩容的临界值 =容量*负载因子


2.底层Entry数组
transient Entry<K,V>[] table; //table是存储元素的数组名
Entry(int h, K k, V v, Entry<K,V> n) 
{
       value = v;
       next = n;
       key = k;
       hash = h;
}

3.构造器
//因为第1/2个构造器都调用了第3个构造器,所以我们只分析第3个构造器
public HashMap() //空参构造器创建默认大小(16)默认负载因子(0.75)的HashMap
{
     this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity) //创建initialCapacity大小,默认负载因子的Hashmap
{
     this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) 
{
     if (initialCapacity < 0)//初始化容量小于0则抛出异常
         throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
     if (initialCapacity > MAXIMUM_CAPACITY) //初始化容量大于1<<30 则用1<<30初始化容量
         initialCapacity = MAXIMUM_CAPACITY;
     if (loadFactor <= 0 || Float.isNaN(loadFactor))
         throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
         
     //hashMap底层创建的数组大小一定是2的n次幂,不一定是传入的形参
      //比如传入的capacity是31 那么实际造的数组大小是32   
     int capacity = 1;
     while (capacity < initialCapacity)
           capacity <<= 1; 
      

     this.loadFactor = loadFactor;//将传入的负载因子赋值给当前对象的加载因子
     
     //threshold:影响扩容的临界值 
     //( 例如空参构造器底层创建长度为16的数组,HashMap并不等到添加第17个时才扩容,而是达到扩容临界值(16*0.75=12)时就扩容)
     threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
     
     table = new Entry[capacity];//创建capacity大小的Entry数组 table是Entry数组名
     useAltHashing = sun.misc.VM.isBooted() &&(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
     init();
}

4.添加元素 put()
public V put(K key, V value) 
{
     if (key == null)//因为可以存储NULL值,所以当传入的key是NULL时,也放入HashMap
     return putForNullKey(value);
     int hash = hash(key); //计算key的hash值
        
     //求已经计算出来的hash值应该在底层数组中存放的位置,
     //table.length是当前底层数组的长度(前面我们已经知道底层数组的长度一定是2的n次幂)
     int i = indexFor(hash, table.length);//请转5查看解析
                
     //利用for循环将要添加的key与 Entry数组中的第i个数据(链)依次进行比较(即key与table[i](或其链上的元素)进行比较)
     for (Entry<K,V> e = table[i]; e != null; e = e.next)
     {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
             {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
              }
      }
     //1.初始化table[i],发现table[i]==NULL,则表明此位置没有数据,跳出for循环,直接调用addEntry()进行添加
     //2.初始化table[i],发现table[i]!=NULL,表明此位置有数据,则转入if语句
     //  多次循环都是 e.hash==hash不成立,表明要添加的key与table[i]对应的数据链上的每一个key都不相同。最后跳出for循环,调用addEntry()进行添加
     //3.初始化table[i],发现table[i]!=NULL,表明此位置有数据,则转入if语句
     //  某次循环发现 e.hash==hash成立,表明要添加的key与table[i]对应的数据链上的某一个key的hash值相同
     //  然后调用equals()方法判断值是否相同。
     //  若不同则if语句返回false,继续循环直到跳出循环,随后调用addEntry()进行添加
     //  若相同则if语句返回true,进入if语句。将要添加的value替换具有相同hash值和内容的value 
        
     //为何使用这句话 ((k = e.key) == key || key.equals(k))
     // 为了加快比较速度,先进行地址比较(因为若地址相同就不用比较内容了)
     modCount++;
     addEntry(hash, key, value, i);//请转6查看解析
     return null;
}

5.计算索引 indexFor()
static int indexFor(int h, int length) 
{
     return h & (length-1); 
}
//Q:为何length-1再和h进行与操作
//A:为了保证计算得到的下标在底层数组中存放的位置不能超出当前数组长度
//比如length=16,则length-1=15,用二进制表示是1111(即低4位全部是1,高位全部是0)
//不论hash值是多少,和15进行与操作后只有低4位不为0(转换成十进制后一定小于16,所以肯定小于当前数组的长度)

6.向数组中添加 addEntry()
void addEntry(int hash, K key, V value, int bucketIndex) 
{
     //if语句主要是扩容相关
     //如果已经存放的数据长度大于临界值 而且 对应的数组处的值(不为空) 则进行扩容(若对应的数组处的值为空则直接添加)
     if ((size >= threshold) && (null != table[bucketIndex])) 
     {
         resize(2 * table.length); //扩容为当前数组长度的2倍
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);
      }
      //数据链上添加元素
      createEntry(hash, key, value, bucketIndex);//请转7查看解析 
}

7.数据链上添加元素 creatEntry()
void createEntry(int hash, K key, V value, int bucketIndex) 
{
     //因为JDK7 是新的元素指向旧的元素
     Entry<K,V> e = table[bucketIndex];//先将新元素要添加的位置上存放的旧元素
     
     //新建一个Entry对象,hash、key、value都是要添加的新元素的相应属性,
     //e是刚才取出来的旧元素(即新元素指向刚才取出来的旧元素,与JDK7是新的元素指向旧的元素吻合)
     //最终结果是将要添加的新元素放入对应的位置,指向新元素
     table[bucketIndex] = new Entry<>(hash, key, value, e);
     size++;
}

3. 源码解析(JDK8)

3.1 概述

  • key-value 构成Node数组
  • 底层存储结构:数组+链表+红黑树
  • 添加元素时链表指向:旧元素指向新元素
    在这里插入图片描述

3.2 添加元素过程

在这里插入图片描述

3.3源码分析

1.变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化容量
static final int MAXIMUM_CAPACITY = 1 << 30;//最大支持容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子
static final int TREEIFY_THRESHOLD = 8;//Bucket中链表长度大于该默认值则转化为红黑树
static final int UNTREEIFY_THRESHOLD = 6;//Bucket中红黑树存储的Node小于该默认值则转化为链表
static final int MIN_TREEIFY_CAPACITY = 64;//桶中的Node被树化时最小的hash表容量
///当哈希表的大小超过64且Bucket中链表长度大于8,才会把链式结构转化成红黑树,否则仅采取扩容来尝试减少冲突
 
2.底层Node数组
Node(int hash, K key, V value, Node<K, V> next) 
{
     this.hash = hash;
     this.key = key;
     this.value = value;
     this.next = next;
}

3.构造函数 //构造器都没有创建HashMap
public HashMap() 
{
     //未创建HashMap。当前对象的负载因子初始化为默认负载因子
     this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

public HashMap(int initialCapacity) 
{ 
     //未创建HashMap。将initialCapacity初始化为大于等于他的最小的2的指数值(比如initialCapacity=45,实际是64),当前对象的负载因子初始化为默认负载因子
     this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) 
{
     if (initialCapacity < 0) //初始化容量小于0则抛出异常
         throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
     if (initialCapacity > MAXIMUM_CAPACITY) //初始化容量大于1<<30 则用1<<30初始化 
         initialCapacity = MAXIMUM_CAPACITY;
     if (loadFactor <= 0 || Float.isNaN(loadFactor))
         throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
      this.loadFactor = loadFactor;//负载因子赋值
      this.threshold = tableSizeFor(initialCapacity);//初始化容量赋值
}

4.计算初始化容量 tableSizeFor()
// 获得第一个大于等于cap的2的幂次方的数(例如cap=34,则返回64)
static final int tableSizeFor(int cap) 
{
      int n = cap - 1;
      n |= n >>> 1;
      n |= n >>> 2;
      n |= n >>> 4;
      n |= n >>> 8;
      n |= n >>> 16;
      return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

5.put()方法
public V put(K key, V value) 
{
      //主要调用putVal()方法(请查看6)、hash()方法请查看7
      return putVal(hash(key), key, value, false, true);
}

6.putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) 
{
       Node<K, V>[] tab;
       Node<K, V> p;
       int n; //hash数组的长度
       int i; //在hash数组中的映射下标
     
       //如果table为空 或 table 长度为0 则进行扩容(正如我们已经知道调用构造器并不会创建HashMap,第一次调用put()时才初始化容量)
       Q:为何调用构造器时不初始化而是选择put()时才初始化?
       A:延迟初始化逻辑以减小内存占用(因为某些hashMap new出来之后并不会立即使用,那么put()时再初始化容量就可以减小内存占用)
       
       if ((tab = table) == null || (n = tab.length) == 0)
           n = (tab = resize()).length;//resize扩容函数请转到8查看
           
       //(n-1 & hash) 是在计算key应该映射到table数组的下标(n在上一步已经得到)
       if ((p = tab[i = (n - 1) & hash]) == null)//插入成功情况1:如果计算得到的下标处对应的元素是NUll(表明无元素)
            tab[i] = newNode(hash, key, value, null);//则直接new (key-value)对象并放置在bucket
       else //计算得到的下标处对应的元素是不为空
       {
            Node<K, V> e;    K k;
            
            //上一步的if已经得到了p = tab[i = (n - 1) & hash]
            //此时指针指向table[i],如果key的hash值与table[i]相同且内容相同就将table[i]赋值给e然后进行替换(替换语句在后面)
            //否则就有可能是红黑树或者链表,以下两种情况就是红黑树或者链表
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) 
                e = p;
            else if (p instanceof TreeNode) //p已经被树化
                    e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);//插入成功情况2:直接在红黑树上插入key-value(如果发现相同key也跳入替换语句)
            else { //此种情况是链表
                  for (int binCount = 0; ; ++binCount) 
                  {
                      if ((e = p.next) == null) //插入成功情况3:已经遍历到末尾,未发现与要插入的key一致的Node,则直接new (key-value)对象并放置在末尾
                      {
                         p.next = newNode(hash, key, value, null);
                         if (binCount >= TREEIFY_THRESHOLD - 1)//如果遍历到最后发现结点数>8 那么就触发树化操作(树化时还要保证hash表容量大于64)
                         { 
                             treeifyBin(tab, hash);
                         }
                        break;//都遍历到末尾了而且也添加了那么就退出循环操作
                       }
                       
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))//遍历链表过程中找到了相同key的Node元素,则需要替换(跳转到替换语句)
                    {
                        break;
                    }
                    
                    p = e;
                  }
            }

            if (e != null) //替换操作:发现了一个与key相同Node的元素,则将要插入的key对应的value替换原来的value
            { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
               {
                   e.value = value;
               }
                afterNodeAccess(e);
                return oldValue;
            }
       }
       ++modCount;//表示散列表被修改的次数(替换Node元素的value不计数)
       if (++size > threshold)//如果存储的key-value数量大于扩容阈值则进行扩容
            resize();
       afterNodeInsertion(evict);
       return null;
}

7.hash()方法
public native int hashCode();//hash()方法里调用此native方法(本地实现)
static final int hash(Object key) 
{
      int h;
      //不难发现,计算hash值时并不是直接调用Object类里的hashcode()方法
      //而是调用hashcode()方法得到hash值后右移16位再和得到的hash值进行异或操作
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
前置知识:与JDK7一致,JDK8在调用hash()方法后得到hash值,然后计算需要放置的key应该映射到哪个桶上时运用 ( hash & n-1 )

Q:hash()方法为何此处要这么处理hash值而不直接调用hashcode()方法得到hash值?
A:目的是减小哈希冲突。例如当n很小时(假设为64,一般hashcode()方法得到的hash值都很大),那么 n-1
即为 630x111111),这样的值跟 hashCode()直接进行与操作,实际上只使用了哈希值的后6(因为0x111111高位全部是0,进行与操作后只有后6位不为0)
如果当哈希值的高位变化很大而低位变化很小,因为没有把高位利用起来,这样就很容易造成哈希冲突,所以这里把高低位都利用起来从而解决了这个问题。

8.resize() 方法
//待解析

4. 关于HashMap的说明

  • Entry / Node 数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素
  • 默认创建数组的容量是16
    默认负载因子是0.75
    默认扩容临界值12 ( 16×0.75 )
  • 向HashMap中添加key相同,value不同的对象时会覆盖原来的value(比如已经添加了key1-value1 再添加key1-value2,那么最终只有key1-value2)
  • hashMap底层创建的数组大小一定是2的n次幂,不一定是传入的形参。
    比如传入的capacity是31 那么实际造的数组大小是32
  • Q:为何hashMap底层创建的数组大小一定是2的n次幂?
    A:我们已经知道得到key的hash值后计算映射位置的时候使用(n-1) & hash ,其中n是底层数组的长度。因为hash值是不固定的,所以说key的hash值的二进制数任何位都可能是0也可能是1。那么要想保证尽量减少hash碰撞,而且充分占据每个数组的位置,必须要保证 数组大小-1 的二进制全是1,如此才能保证最后的运算结果,完全取决于hash的二进制数,也就是最后的结果会保证每个位都有可能是0或1。而一个十进制数,如果它是2的n次幂,那么它减一后的二进制数就都是1
  • 若已经存放的数据长度大于临界扩容值 而且 对应的数组处的值(不为空) 则进行扩容,默认扩容为当前数组长度的2倍
  • Q:为什么需要扩容?
    A:解决 hash冲突导致的链化从而影响查询效率。比如hash表的长度为8,不扩容的情况下可能链上元素过多导致查询效率由o(1)上升到o(n)
  • JDK8中当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时此时此索引位置上的所数据改为使用红黑树存储。若知识链表形式存在的数据个数 > 8 但是当前数组的长度 <=64时会扩容当前数组如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表
  • JDK7 调用构造函数就创建了hashMap
    ( 比如new HashMap() 就已经创建了长度为16,负载因子为0.75的Entry数组)
    JDK8调用构造函数时未创建hashMap而是调用put( )时才创建
    (比如new Hashmap()后再put()就创建了长度为16,负载因子为0.75的Node数组)
  • 以JDK8为例:计算hash值时并不是直接调用Object类里的hashcode()方法而是调用hashcode()方法得到hash值后右移16位再和得到的hash值进行异或操作得到最终的hash值
  • Q:hash()方法为何此处要这么处理hash值而不直接调用hashcode()方法得到hash值?
    A:目的是减小哈希冲突。例如当n很小时(假设n为64,一般hashcode()方法得到的hash值都很大),那么 n-1即为 63(0x111111),这样的值跟 hashCode()直接进行与操作,实际上只使用了哈希值的后6位(因为0x111111高位全部是0,进行与操作后只有后6位不为0)如果当哈希值的高位变化很大而低位变化很小,因为没有把高位利用起来,这样就很容易造成哈希冲突,所以这里把高低位都利用起来从而解决了这个问题。
  • Q:计算出hash值之后再计算对应的数组位置时 为何length-1再和h进行与操作
    A:为了保证计算得到的下标在底层数组中存放的位置不能超出当前数组长度
    比如length=16,则length-1=15,用二进制表示是1111(即低4位全部是1,高位全部是0)
    不论hash值是多少,和15进行与操作后只有低4位不为0(转换成十进制后一定小于16,则肯定小于当前数组的长度)
  • Q:调用equals()时为何使用 ( (k = e.key) == key || key.equals(k) )
    A:为了加快比较速度,先进行地址比较(因为若地址相同就不用比较内容了)
  • Q:为何源码大量使用位运算符而不使用算数运算符(±×÷)
    A:因为位运算符能提升运行速度。计算机底层是操作二进制数,使用位运算符能直接操作二进制从而加快速度
  • Q:负载因子值的大小对HashMap有什么影响
    A负载因子的大小决定了HashMap的数据密度。
          负载因子越大则密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
          负载因子越小就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能
          按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. C 语言中的指针和内存泄漏 5 2. C语言难点分析整理 10 3. C语言难点 18 4. C/C++实现冒泡排序算法 32 5. C++中指针和引用的区别 35 6. const char*, char const*, char*const的区别 36 7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式的执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇 107 21. C语言编程常见问题分析 108 22. C语言编程易犯毛病集合 112 23. C语言缺陷与陷阱(笔记) 119 24. C语言防止缓冲区溢出方法 126 25. C语言高效编程秘籍 128 26. C运算符优先级口诀 133 27. do/while(0)的妙用 134 28. exit()和return()的区别 140 29. exit子程序终止函数与return的差别 141 30. extern与static存储空间矛盾 145 31. PC-Lint与C\C++代码质量 147 32. spirntf函数使用大全 158 33. 二叉树的数据结构 167 34. 位运算应用口诀和实例 170 35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 51. 程序实例分析-为什么会陷入死循环 258 52. 空指针究竟指向了内存的哪个地方 260 53. 算术表达式的计算 265 54. 结构体对齐的具体含义 269 55. 连连看AI算法 274 56. 连连看寻路算法的思路 283 57. 重新认识:指向函数的指针 288 58. 链表源码 291 59. 高质量的子程序 295 60. 高级C语言程序员测试必过的十六道最佳题目+答案详解 297 61. C语言常见错误 320 62. 超强的指针学习笔记 325 63. 程序员之路──关于代码风格 343 64. 指针、结构体、联合体的安全规范 346 65. C指针讲解 352 66. 关于指向指针的指针 368 67. C/C++ 误区一:void main() 373 68. C/C++ 误区二:fflush(stdin) 376 69. C/C++ 误区三:强制转换 malloc() 的返回值 380 70. C/C++ 误区四:char c = getchar(); 381 71. C/C++ 误区五:检查 new 的返回值 383 72. C 是 C++ 的子集吗? 384 73. C和C++的区别是什么? 387 74. 无条件循环 388 75. 产生随机数的方法 389 76. 顺序表及其操作 390 77. 单链表的实现及其操作 391 78. 双向链表 395 79. 程序员数据结构笔记 399 80. Hashtable和HashMap的区别 408 81. hash 表学习笔记 410 82. C程序设计常用算法源代码 412 83. C语言有头结点链表的经典实现 419 84. C语言惠通面试题 428 85. C语言常用宏定义 450
HashMapJava中常用的数据结构,用于存储键值对,并支持O(1)间复杂度的插入、查询、删除等操作。 HashMap源码解析如下: 1. HashMap一个实现了Map接口的类,内部使用数组和链表实现。 2. HashMap中的键值对是以Entry对象的形式存储的,每个Entry对象包含一个键、一个值和指向下一个Entry对象的引用。 3. HashMap内部维护了一个默认容量为16的数组table,负载因子为0.75,默认扩容因子为2。当HashMap中的元素数量超过容量与负载因子的乘积,即会触发扩容操作。 4. HashMap使用哈希函数将键映射到对应的数组下标上,实现快速查询。 5. 如果哈希函数产生了哈希冲突,即多个键映射到同一个数组下标上,HashMap会使用链表将这些键值对串起来,以便查询遍历链表查找。 6. 在插入新的键值对HashMap会根据哈希函数计算出对应的数组下标,并将新的键值对插入到该位置链表中。如果该位置链表长度超过阈值(默认为8),则将这个链表转化为红黑树,以提高查询效率。 7. 在查询、删除键值对HashMap根据哈希函数计算出对应的数组下标,并遍历该位置链表或红黑树,查找对应的键值对。如果链表或红黑树中没有对应的键值对,则返回null。 总之,HashMap一个高效的数据结构,能够快速地插入、查询、删除键值对。不过,对于高度散列的数据集,也可能导致哈希冲突的增加,进而导致查询效率下降。因此,在使用HashMap,需要合理地设置容量和负载因子,以及注意键的哈希函数的设计。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值