HashMap源码解析

一、HashMap介绍

HashMap位于JDK自带jar包rt.jar的java.util目录下。 HashMap是一个散列表,存储的内容是键值对<key,value>映射。

HashMap继承于AbstractMap,实现了Map、Cloneable、Serializable接口 HashMap是线程不安全的,其中key、value都可以为null,且是无序的。

二、HashMap的数据结构

HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

我们看看HashMap中Entry类的代码:

HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

三、HashMap源码分析

1、关键属性

先看看HashMap类中的一些关键属性:

transient Entry[] table;//存储元素的实体数组

transient int size;//存放元素的个数

int threshold; //临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量

final float loadFactor; //加载因子

transient int modCount;//被修改的次数

其中loadFactor加载因子是表示Hsah表中元素的填满的程度.

如:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

冲突的机会越大,则查找的成本越高.

因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.

如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

2、构造方法

下面看看HashMap的几个构造方法:

public HashMap(int initialCapacity, float loadFactor) {
     //确保数字合法
     if (initialCapacity < 0)
       throw new IllegalArgumentException("Illegal initial capacity: " +
                        initialCapacity);
     if (initialCapacity > MAXIMUM_CAPACITY)
       initialCapacity = MAXIMUM_CAPACITY;
     if (loadFactor <= 0 || Float.isNaN(loadFactor))
       throw new IllegalArgumentException("Illegal load factor: " +
                        loadFactor);
     // Find a power of 2 >= initialCapacity
     int capacity = 1;  //初始容量
     while (capacity < initialCapacity)  //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
       capacity <<= 1; 
     this.loadFactor = loadFactor;
     threshold = (int)(capacity \* loadFactor);
     table = new Entry[capacity];
    init();
  }
   public HashMap(int initialCapacity) {
     this(initialCapacity, DEFAULT_LOAD_FACTOR);
  } 
   public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     threshold = (int)(DEFAULT_INITIAL_CAPACITY \* DEFAULT_LOAD_FACTOR);
     table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
   }

我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂,至于为什么要把容量设置为2的n次幂,我们等下再看。

重点分析下HashMap中用得最多的两个方法put和get

3、存储数据

下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:

上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

我们慢慢地来分析这个函数,第2和3行的作用就是处理key值为null的情况,我们看看putForNullKey(value)方法:

注意:如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。即table[0]

我们再回去看看put方法中第4行,它是通过key的hashCode值计算hash码,下面是计算hash码的函数:

//计算hash值的方法 通过键的hashCode来计算
   static int hash(int h) {
     // This function ensures that hashCodes that differ only by
     // constant multiples at each bit position have a bounded
     // number of collisions (approximately 8 at default load factor).
     h ^= (h >>> 20) ^ (h >>> 12);
     return h ^ (h >>> 7) ^ (h >>> 4);
   }

得到hash码之后就会通过hash码去计算出应该存储在数组中的索引。

踩坑记录

坑:将字符作为key,出现次数作为value存储在了hashmap中,这样当需要找那个只需要出现一次的字符时,只需要找到第一次出现次数为1的字符即可。我使用了HashMap的keySet的迭代器,进行遍历,在输入googl之前结果都是正确的,但是当输入了e之后返回了e。代码如下:

使用本地IDE debug后发现在插入e之后,e的地址在l之前,想了下,hashmap在存入数据时是基于hash算法寻址的,因此并不能保证按顺序存入数据,因此使用hashmap的迭代器并不能找到第一个次数为1字符,只能找到内部数组第一次出现次数为1的字符。更改思路:增加一个数组按顺序存放字符,随后遍历数组,将数组的值作为key获取value(次数),代码如下:

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是Java中常用的数据结构,用于存储键值对,并支持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时,需要合理地设置容量和负载因子,以及注意键的哈希函数的设计。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值