【图解版】HashMap原理初探

本文深入探讨了Java中HashMap的内部工作原理,包括其基于哈希表的实现方式、哈希算法的应用、散列碰撞处理策略及优化技巧。适用于希望深入了解HashMap内部机制的开发者。
  • 参考博客

Java中的equals和hashCode方法详解
链表
java提高篇(二三)—–HashMap Java
HashMap的工作原理

算法的时间复杂度和空间复杂度-总结
Hashmap
Hash (散列函数)
第1部分 HashMap介绍
HashCode的作用原理和实例解析
hash碰撞处理
高性能场景下,HashMap的优化使用建议

  • 整体思路

	 1. 什么是HashMap?有什么作用?
	 2. 了解HashMap原理前,需要知道哪些知识?
	 3. 分析HashMap原理。
	 4. 应用场景。
  • 什么是HashMap?有什么作用?

什么是HashMap: 基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
作用:通过哈希算法,通过键值可快速找到自己需要的对象。

  • 了解HashMap原理前,需要知道哪些知识??


  • 创建一个测试类,创建一个hashmap随机插入一个数据,打入断点查看hashmap的结构这里写图片描述
    1.有一个table数组,长度为16,随机存放之前put进去的key,value对象,这个对象的名称叫Entry
    2.Entry包含了四个对象,hash,key,next,value
    创建一个对象EntryTest,重写他的hashCode方法,直接写死让所有的返回值都是1
    这里写图片描述
    随机插入几个数据,查看hashmap结构这里写图片描述
    这个时候,虽然插入3个对象table的数据长度为1,点开Entry中的next属性发现每个next都存放着一个Entry对象,这便是链表形式的存储数据,hashmap采用的是哈希函数中拉链法存储数据,数组+链表。查看源码中的addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
        //获取bucketIndex处的Entry
        Entry<K, V> e = table[bucketIndex];
        //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        //若HashMap中元素的个数超过极限了,则容量扩大两倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }

了解基本的hashmap结构,我们主要从拉链法来分析hashmap的put和get方法。查看put源码:

/**
  * Associates the specified value with the specified key in this map. If the
  * map previously contained a mapping for the key, the old value is
  * replaced.
  *
  * @param key
  *            key with which the specified value is to be associated
  * @param value
  *            value to be associated with the specified key
  * @return the previous value associated with <tt>key</tt>, or <tt>null</tt>
  *         if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return
  *         can also indicate that the map previously associated
  *         <tt>null</tt> with <tt>key</tt>.)
  */
 public V put(K key, V value) {
  if (key == null)
   return putForNullKey(value);
  int hash = hash(key.hashCode());
  int i = indexFor(hash, table.length);
  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;
   }
  }
 
  modCount++;
  addEntry(hash, key, value, i);
  return null;
 }

1.对key做null的检查,如果为null,会被存储在table[0]中,因为null的hash值总是为0
2.计算好hash值,hash值即为存在table数组中的索引,为了考虑到table数组的均匀分配,最好能保证每个里面都能分配到一个,增加查询速度
3.indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引,HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。至于为什么是2的n次方点击这篇博客。
4.通过hash算法可以知道,如果2个key有相同的hash值,此为hash碰撞,相同的hash值会以链表的形式存储:

如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。

这里要注意两个问题:

一是链的产生。这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。
二、扩容问题。 随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度加载因子,默认是0.75也就是说160.75=13就开始扩容了。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

  • bucketIndex相当于缓冲池的作用,避免了每次产生Entry对象是都去操作table数组,对象在产生的时候只需要去关心bucketIndex位置的元素就可以完成整个数组元素的插入和创建
  • 为了防止链表的长度过长,系统会设置临界点进行扩容,临界点为table数组长度*加载因子。但是扩容是一个非常耗时的过程如果我们预知hashmap中的元素进行设计就能有效提高hashmap性能

put方法的实现还是围绕数组加链表的形式,需要注意的是碰撞和扩容的问题。接下来是存储的问题,查看put方法的实现:

public V get(Object key) {
        // 若为null,调用getForNullKey方法返回相对应的value
        if (key == null)
            return getForNullKey();
        // 根据该 key 的 hashCode 值计算它的 hash 码  
        int hash = hash(key.hashCode());
        // 取出 table 数组中指定索引处的值
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            //若搜索的key与查找的key相同,则返回相对应的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

围绕数组加链表结构,通过key的hash值找到table数组中的Entry ,key是以String的形式存储在Entry对象中的,只需要使用equals方法匹配到对应链表中的key取到value即可获取到value的值:
这里写图片描述

  • 应用场景

  • 如何优化

1.考虑加载因子地设定初始大小
2.减小加载因子
3.String类型的key,不能用==判断或者可能有哈希冲突时,尽量减少长度
4.使用定制版的EnumMap
5.使用IntObjectHashMap

优化从提高查询效率和减少系统开销分析。

加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。

对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;
如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值