java----hashmap底层原理

article/2025/9/22 0:37:59

概述

在Java集合中,Map是一种特殊的集合,原因在于这种集合容器并不是保存单个元素,而是保存一个一个的Key-Vaue键值对.HashMap是基于哈希表的Map接口的实现,在项目开发中使用广泛,下面就对HashMap的源码进行解析.

Hashmap的特点

1.HashMap是基于哈希表的Map实现.
2.HashMap底层采用的是Entry数组(1.7)和链表实现.
3.HashMap是采用key-value形式存储,其中key是可以允许为null,但是只能有一个,并且key不能重复.
4.HashMap是线程不安全的.
5.HashMap存入数据的顺序和遍历的顺序有可能是不一样的.

在HashMap中存在很多的方法,在此我们只对添加、删除、遍历等方法进行解析.以便了解其原理.

Hashmap的数据结构
在数据结构中,有数组和链表来实现对数据的存储,但这两者基本上是两个极端.

  • 数组: 数组储存区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1);
    数组的特点是寻址容易,插入和删除困难
  • 链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N).
    链表的特点是寻址困难,插入和删除容易

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是哈希表,哈希表即满足了数据查找的方便,同时不占用太多的内存空间,使用也十分方便.

HashMap底层使用的就是哈希表.

HashMap实际上是一个"链表"的数组,每个数组中的元素存放链表的头结点,在每一个头结点的中,包含着下一个节点的地址,即数组和链表的结合体.

在这里插入图片描述
更为详细的结构如下:
在这里插入图片描述
从上图可以看到,HashMap底层就是一个数组结构,数组中的每一项又是一个链表.

原理

1、HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

当两个不同的键对象的hashcode相同时会发生什么?
它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

区别:
(1)时间
hashTable是java发布的时候提供的键值映射的数据结构、hashMap是在jdk1.2之后出现的。但是hashTable现在几乎弃用,虽然它是线程安全,但是ConcurrentHashMap却可以替代并且效率更好
(2)提供的接口不同
hashtable相比hashMap多提供了2个接口elements()和contains()。其中elements()返回hashTable的中value的枚举。contains()判断传入的value是否包含在hashTable中。
(3)继承的类不同
hashMap继承AbstractMap类、而hashTable继承自Dictionary类。相同的是同实现了map、Cloneable、Serializable接口
(4)HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
(5)HashMap是非synchronized, 而Hashtable是synchronized, 这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
(6)另一个区别是HashMap的迭代器(Iterator)是fail-fast(快速失败)迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。但是在jdk8之后hashTable的迭代器也加入了fail-fast迭代器。
(7)初始容量大小和每次扩充容量大小的不同
Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
之所以会有这样的不同,是因为Hashtable和HashMap设计时的侧重点不同。Hashtable的侧重点是哈希的结果更加均匀,使得哈希冲突减少。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而HashMap则更加关注hash的计算效率问题。在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改动。这从而导致了Hashtable和HashMap的计算hash值的方法不同 。
(8)计算hash值的方法不同
为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。

Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。

Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高,而计算位运算。
为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然了,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2 的幂次方带来的效率提升给抵消掉。
HashMap的部分源码:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{//初始化桶大小,也就是数组的大小,默认大小为16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//桶的最大值static final int MAXIMUM_CAPACITY = 1 << 30;//默认的负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;//存放数据的数组transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//存储key-value键值对的个数transient int size;//桶大小,在初始的时候可以显式指定(一定是2的次幂)int threshold;//负载因子,初始化时可以显式指定final float loadFactor;//修改次数,每次map集合变动一次,就加1transient int modCount;//真正存放数据的entry内部类static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;...省略其他}

HashMap的构造函数

  • HashMap(): 构造一个具有默认初始容量(16)和默认加载因子(0.75)的空HashMap

  • HashMap(int initialCapacity): 构造一个带指定初始容量和默认加载因子的空HashMap.

  • HashMap(int initialCapacity, float loadFactor): 构造一个带指定初始容量和指定负载因子的空HashMap.

  • HashMap(Map<? extends K, ? extends V> m): 根据指定的map集合创建一个HashMap.

代码实例:

public HashMap(int initialCapacity, float loadFactor) {//如果初始容量小于0,则抛出一个异常if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);//如果指定的初始化大小大于最大值,则将容量置为最大值.if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;//如果负载因子不是数字或者小于等于0,抛出异常if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);//让当前map的容器大小和加载因子等于指定的值	this.loadFactor = loadFactor;threshold = initialCapacity;//初始化方法,在HashMap中没有实现,其子类有具体实现.init();}

我们看到,在初始化的时候,没有为table数组分配内存空间,而是在put操作的时候才真正构建table数组.

初始容量和负载因子

在HashMap的属性中,有两个参数:初始容量,负载因子.

这两个参数是影响HashMap性能的重要参数.

其中容量表示哈希表中桶的数量,也就是数组的长度.初始容量是创建哈希表时的容量,如果不指定,默认是16.

加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它可以衡量一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之越小.

查看put方法的源码可知:
当哈希表中数据的数量超出了当前容量*加载因子时,对该HashMap进行扩容,将容量扩充至两倍的桶数
HashMap的put方法

    public V put(K key, V value) {//如果table数组为空{},则为table初始化分配内存空间,入参为threshold,默认是16if (table == EMPTY_TABLE) {inflateTable(threshold);}//如果key为null,保存null于table的第一个位置,也就是table[0]if (key == null)return putForNullKey(value);//根据ket计算出hash值int hash = hash(key);//计算出该key所应该保存的桶位置,也就是在数组table中的位置int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//遍历该索引位置的桶链表,如果存在相同的key,用老值替换新值,返回老值if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}//修改次数增加1modCount++;//根据key-value新增一个Entry对象写入当前位置addEntry(hash, key, value, i);return null;}

通过源码我们可以很清晰的看到put方法的执行逻辑:

  • 首先判断HashMap中的table表是不是为空,如果为空,调用inflateTable(threshold)方法为table分配内存空间
  • 然后判断key是否为空,如果key为空,则调用putForNullKey(value)方法,将value放在数组的第一个位置上.
  • 若key不为空,则根据hash(key)方法计算出hash值,然后根据hash值,得到这个元素在table数组中的位置(下标),如果table在该位置已经存放了其他元素,则通过比较是否存在相同key,若存在则覆盖原来key的value,否则将该元素保存在链头.
  • 若table所在该处没有元素,那就直接将该元素放到此数组中的该位置上.

至此完成来了put方法的全过程
HashMap的get方法

    public V get(Object key) {//如果key为null,直接去table[0]处检索.if (key == null)return getForNullKey();//查找出map中对应key的entry对象Entry<K,V> entry = getEntry(key);//如果value不存在就返回null,否则返回对应的valuereturn null == entry ? null : entry.getValue();}

在具体的getEntry()方法中:

    final Entry<K,V> getEntry(Object key) {if (size == 0) {return null;}//计算出key对应的hash值int hash = (key == null) ? 0 : hash(key);//获取最终数组中的索引,遍历链表,通过equals方法找出对应的entry对象返回.for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;}

get方法相对比较简单:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。


http://chatgpt.dhexx.cn/article/CfgQ462s.shtml

相关文章

HashMap底层数据结构详解

一、HashMap底层数据结构 JDK1.7及之前&#xff1a;数组链表JDK1.8&#xff1a;数组链表红黑树 关于HashMap基本的大家都知道&#xff0c;但是为什么数组的长度必须是2的指数次幂&#xff0c;为什么HashMap的加载因子要设置为0.75&#xff0c;为什么链表长度大于等于8时转成了…

复习一波HashMap底层实现原理解析

HashMap是JAVA中最常见的集合类框架&#xff0c;也是java语言中非常典型的数据结构&#xff0c;同时也是我们需要掌握的数据结构&#xff0c;更重要的也是面试题必问之一。 我们常见的有集合数据有三种结构&#xff1a;1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各…

HashMap的底层实现

1. HashMap概述&#xff1a; HashMap是基于哈希表的Map接口的非同步实现&#xff08;Hashtable跟HashMap很像&#xff0c;唯一的区别是Hashtalbe中的方法是线程安全的&#xff0c;也就是同步的&#xff09;。此实现提供所有可选的映射操作&#xff0c;并允许使用null值和null键…

HashMap底层特性全解析

文章目录 一、前言二、HashMap2.1 HashMap数据结构2.2 HashMap线程不安全2.3 哈希冲突 三、JDK1.7中HashMap的实现3.1 基本元素Entry3.2 插入逻辑3.2.1 插入逻辑3.2.2 新建节点添加到链表 3.3 数组扩容逻辑3.4 null处理3.5 辨析扩容、树化和哈希冲突 四、JDK1.8中HashMap的实现…

HashMap底层

1、HashMap底层数据结构 JDK1.7的底层是 数组链表&#xff1b; JDK1.8之后 数组 链表 红黑树&#xff1b; 数组特点&#xff1a;具有随机访问的特点&#xff0c;能达到O(1)的时间复杂度&#xff0c;数组查询快&#xff0c;增删比较麻烦&#xff1b; 链表特点&#xff1a;…

HashMap底层实现和原理(源码解析)

Note&#xff1a;文章的内容基于JDK1.7进行分析&#xff0c;1.8做的改动文章末尾进行讲解。 大家可以看一下:https://www.imooc.com/article/267756 一、先来熟悉一下我们常用的HashMap 1、概述 HashMap基于Map接口实现&#xff0c;元素以键值对的方式存储&#xff0c;并且…

HashMap 底层原理

前言 HashMap 源码和底层原理在现在面试中是必问的。因此&#xff0c;我们非常有必要搞清楚它的底层实现和思想&#xff0c;才能在面试中对答如流&#xff0c;跟面试官大战三百回合。文章较长&#xff0c;介绍了很多原理性的问题&#xff0c;希望对你有所帮助~ 正文 **说明&a…

HashMap底层原理剖析(面试收藏!!!)

HashMap HashMap底层原理剖析(超详细&#xff01;&#xff01;&#xff01;)一、散列表结构二、什么是哈希&#xff1f;三、HashMap原理讲解3.1继承体系图3.2Node数据结构分析3.3底层存储结构3.4put数据原理分析3.5什么是哈希碰撞&#xff1f;3.6JDK8为什么引入红黑树&#xff…

HashMap底层原理

文章目录 1.HashMap的概念2.底层数据结构2.JDK1.8之前存在的问题&#xff1f;3.问题&#xff1a;加载因子为什么默认值为0.75f &#xff1f;4.问题&#xff1a;如果得到key的hash值&#xff08;哈希码&#xff09;5.问题&#xff1a;如何得到插入元素在数组中的下标6.问题&…

HashMap 的底层结构和原理

1. 讲讲 HashMap 的底层结构和原理 HashMap 就是以 Key-Value 的方式进行数据存储的一种数据结构嘛&#xff0c;在我们平常开发中非常常用&#xff0c;它在 JDK 1.7 和 JDK 1.8 中底层数据结构是有些不一样的。总体来说&#xff0c;JDK 1.7 中 HashMap 的底层数据结构是数组 …

HashMap底层原理(详细介绍)

数组&#xff1a;其实所谓的数组指的就是一组相关类型的变量集合&#xff0c;并且这些变量彼此之间没有任何的关联。存储区间连续&#xff0c;占用内存严重&#xff0c;数组有下标&#xff0c;查询数据快&#xff0c;但是增删比较慢&#xff1b; 链表&#xff1a;一种常见的基…

HashMap底层详解

1、HashMap底层存储原理详解 HashMap存储原理 ☆获取到传过来的key&#xff0c;调用hash算法获取到hash值 ☆获取到hash值之后调用indexFor方法&#xff0c;通过获取到的hash值以及数组的长度算出数组的下标 (把哈希值和数组容量转换为二进&#xff0c;再在数组容量范围内与哈…

Java基础——工厂模式、单例模式、懒汉模式、饿汉模式

案例&#xff1a; 这里有Factory类、Goods接口、Foods类、Drink类以及Others类。其中&#xff0c;Foods类、Drink类和Others类继承Goods接口&#xff0c;实现各自对应的方法。然后&#xff0c;在测试类中&#xff0c;创建Goods接口指向三个子类中的某一个&#xff0c;通过Facto…

单例模式——饿汉模式和懒汉模式

目录 &#x1f95d;线程安全的单例模式&#x1f95d;饿汉模式&#x1f95d;懒汉模式&#x1f95d; 懒汉模式总结 &#x1f95d;线程安全的单例模式 线程安全的单例模式是面试中常见的问题,所以熟练掌握这种单例模式尤为重要 什么叫单例模式? 单例模式就是一种设计模式,写代码时…

C# 设计模式之单例模式(懒汉模式、饿汉模式、静态内部类模式)

C# 设计模式之单例模式&#xff08;懒汉模式、饿汉模式、静态内部类模式&#xff09; 应用场景&#xff1a;在整个软件运行生命周期内&#xff0c;一个类只允许一次实例化&#xff0c;例如数据库连接池的连接对象创建&#xff1b;通过使用单例模式来避免反复创建连接对象&#…

muduo源码剖析——Singleton单例模式之懒汉模式与DCL双重检查

0 懒汉与饿汉 对于Singleton单例模式我们并不陌生&#xff0c;但我们常用的多是饿汉模式&#xff1a; Singleton实例的声明和实例化在instance()函数中同时完成。而懒汉模式要求&#xff0c;Singleton实例的声明和实例化分开&#xff1a; 先声明Singleton实例对象&#xff0…

C++单例模式 : 懒汉模式 与 饿汉模式

单例模式&#xff1a; 只能有一个实例&#xff0c;有懒汉和饿汉区分&#xff0c;实现核心思想&#xff1a; 1.构造函数私有化 2.使用静态函数作为接口来获取类对象 1、懒汉模式&#xff1a; 由调用者实例&#xff0c;多线程情况下会存在线程安全问题&#xff0c;需要加互斥锁进…

单例模式的创建(饿汉模式懒汉模式)

&#x1f388;专栏链接:多线程相关知识详解 目录 一.什么是单例模式 二.用static来创建单例模式 三.饿汉模式与懒汉模式 四.饿汉模式与懒汉模式的线程安全问题 五.New引发的指令重排序问题 六.小结 一.什么是单例模式 单例模式就是指某个类有且只有一个实例(instance…

单例模式:懒汉模式

所谓“懒汉式”与“饿汉式”的区别&#xff0c;是在与建立单例对象的时间的不同。 “懒汉式”是在你真正用到的时候才去建这个单例对象“饿汉式是在类创建的同时就已经创建好一个静态的对象&#xff0c;不管你用的用不上&#xff0c;一开始就建立这个单例对象 代码实现&#x…

java 单例模式 之懒汉模式

单例模式&#xff1a;一个类&#xff0c;始终仅仅对外提供自己的一个实例&#xff0c;这样的设计方案&#xff0c;就称单例模式。 懒汉模式&#xff1a; 构造函数私有 声明私有的本类静态实例 定义静态的方法&#xff0c;在方法中创建本类实例&#xff0c;并返回该实例 pu…