HashSet、HashMap,散列表数据结构(哈希表)

HashSet和HashMap的底层实现——哈希表、散列表 java.lang  类 Object java.lang.Object int hashCode()            返回该对象的哈希码值。 boolean equals(Object obj)           指示某个其他对象是否与此对象“相等”。 一、概述 java最基本的两种数据结构 阅读详情


前几天介绍了 ArrayList的底层实现以及对性能的简单分析。今天再来看看HashSet,HashMap。

HashSet:

很多开发者,初学者都知道HashSet无序,不可重复,线程非同步。底层是哈希表结构。
但它是怎么做到的?什么是散列表数据结构(哈希表)?有什么特性?都清楚吗?不清楚继续往下看。

它是这样做到的:

先来看HashSet的源码,首先看默认构造器:
public HashSet() {
	map = new HashMap<E,Object>();
}
// ok,我们看到构造器中new了一个HashMap。key使用了泛型,value使用Object。

再来看add方法源码:
private static final Object PRESENT = new Object();
public boolean add(E e) {
	return map.put(e, PRESENT)==null;
}
// PRESENT是一个Object类型的常量,用来当做map的value. 也就是说,你以后在HashSet中存储的元素都是HashMap中key,value全部使用Object。

HashMap的key是不可以重复的,保证元素唯一的依据是对象的hashCode跟equals方法。
而HashSet不就是用HashMap的key来存储元素嘛,也就保证了元素的唯一性。包括迭代器也是HashMap中keySet方法取得的iterator。
public Iterator<E> iterator() {
	return map.keySet().iterator();
}

通过上面的介绍,已经对HashSet比较了解了,我们知道HashSet底层是用了HashMap。
要想知道怎么做到存取速度快的,我们直接看HashMap就好了。

散列表数据结构(哈希表)

散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

HashMap底层就是散列表数据结构,即数组和链表的结合体,
底层是一个数组结构,数组中的每一项又是一个链表。这样做有什么好处呢?
数组能够提供对元素的快速访问但不易于扩展(如果不知道元素脚标,还得进行遍历查找),链表易于扩展但不能对其元素进行快速访问。
怎样做到两全其美,就是散列表数据结构。

我们来看看HashMap中元素存跟取的实现方式:

首先明白,HashMap根据key的hashCode计算出元素在Entry数组中的位置,然后再Entry内部链表中存放key,value。

先看构造方法源码:
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认初始化容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
final float loadFactor; // 用于计算扩容阀值
 /* The next size value at which to resize (capacity * load factor) */
int threshold; // Entry扩容阀值
// The table, resized as necessary. Length MUST Always be a power of two.
transient Entry[] table;// 存放键值对的Entry数组
public HashMap() { 
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); // 计算扩容阀值
    table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 初始化Entry<K,V>数组
    init();
}
/* 在默认构造方法中,初始化了一个容量为16的HashMap(Entry数组),当元素超过75%(16*0.75f=12个)的时候开始自动扩容*/

put方法源码: eg: map.put("a","abc");
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode()); // 获取key的hash值
    int i = indexFor(hash, table.length); // h & (length-1); 通过hashcode取模数组长度, 定位hash值在table数组中的索引
    // 如果table数组中i索引所在位置有元素,循环遍历该链表中的下一个元素
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 遍历到了hash值相同并且equals也相同的key,把value用新值替换掉
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
	
    modCount++;
    // table中i索引所在位置没有元素,添加key、value到指定索引处。
    addEntry(hash, key, value, i);
    return null;
}

addEntry()方法源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
	// 下面两行代码将entry保存进了table数组中Entry内部链表的第一个位置。
	Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold) // 需要扩容了
        resize(2 * table.length); // 重新计算数组大小
}

由于元素的位置是通过hashcode取模数组长度而得, 现在由于需要扩容,数组长度会发生变化,
所以会在resize方法跟transfer方法中进行元素位置的重新分配。

resize()方法源码: // 重新计算数组长度
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }


    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor); // 新的扩容阀值
}

transfer()方法源码:// 重新分配
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

上面是HashMap存元素的实现方式,再来看看取元素的方式:

// get方法源码
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode()); // 还是计算key的hashcode,
    // 定位hash值在table数组中的索引,并通过equals方法定位元素在链表中的位置。
    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.equals(k)))
            return e.value;
    }
    return null;
}

我们来对HashMap存取元素的过程来做一个小的总结:

元素(key,value)在HashMap中被封装进Entry数组。
put元素的时候,根据key的hash值定位元素在Entry数组中的索引,如果当前索引有元素,就通过equals方法进行比较,将元素存进当前Entry的链表中适当的位置。
get元素的时候,根据key的hash值定位元素在Entry数组中的索引,然后通过equals方法定位元素在链表中的位置,取出该元素。

性能分析:

因为元素的存取是通过hash算法进行的,所以速度都没的说。
在查找操作中,唯一影响性能的是在链表中,但实际只要优化好了key对象hashCode跟equals方法,就会避免链表中的数据过多而导致查找性能变慢。

再一个非常影响性能的是数组扩容操作,当使用默认的DEFAULT_INITIAL_CAPACITY对HashMap进行初始化的时候,
如果元素个数非常多,会导致扩容次数增加,每次扩容都会进行元素位置的重新分配,这是相当耗费性能的。
如果能预算好元素个数,就应该避免使用默认的DEFAULT_INITIAL_CAPACITY,可在HashMap的构造函数中为其指定一个初始值。

问题解决:

hashCode必须和equals保持兼容(equals方法的判断依据和计算hashCode的依据相同),这样做是为了避免链表中的数据过多。

举例:  
public class Person {  
       public int id;  
       public String name="";  

       public int hashCode() {   
            return id;  
       }  
       // equals必须比较id  
       public boolean equals(Person p) {  
            if(this.id == p.id)
            	return true;  
            else 
            	return false;  
       }
}

如果元素很多,应该使用这个构造函数public HashMap(int initialCapacity){}对HashMap进行初始化。
举例:HashMap map = new HashMap(1024);

了解了HashMap的存储原理之后,自然也就明白了为什么说HashSet的存取效率高了。


原创文章,转载请注明出处: http://blog.youkuaiyun.com/thinking_in_android
HashSet和HashMap HashMap集合是 Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复且集合中的元素是无序的。HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。水平方向以数组结构为主体并在竖直方向以链表结构进行结合的就是 HashMap中的哈希表结构。 阅读详情

相关推荐

Java数据结构---HashMap(哈希表及其基本操作)(含hashset)

Java数据结构---HashMap(哈希表及其基本操作)1. HashMap简介2. 基础操作(增、删、改、查)3. 其他操作 1. HashMap简介 HashMap 是一个散列表,它存储的内容是键值(key-value)映射。 HashMap 的 key 与 value 类型可以相同也可以不同,根据定义,不受限制。 2. 基础操作(增、删、改、查) 定义一个哈希表 HashMap<Integer, String> hashmap= new HashMap<Integer, Str

cloud的博客 2万+

HashMap与HashSet

Map与Set(末)

c_study__c的博客 3293

微信公众号数据采集实战:历史文章、互动数据与评论详情全解析

本文提供了一套完整的微信公众号数据采集实战方案,详细解析了如何通过接口调用获取公众号的历史文章列表、单篇文章的互动数据以及详细的用户评论。文章从环境准备、核心参数解析到分页逻辑、批量处理和异常处理,提供了可运行的Python代码示例,旨在帮助开发者高效、稳定地构建自动化采集工具,用于竞品分析、内容研究和舆情监控。

u5v6w7的博客 1593

【数据结构】HashMap和HashSet

本期主要讲解:1、HashMap 1.1 HashMap的简介 1.2 HashMap 部分源码 1.2.1 HashMap 属性 1.2.2哈希表的构造方法 1.2.3 HashMap 方法 2、HashSet

拼命阿紫冲冲冲! 2020

数字IC后端新手必看:Innovus从安装到跑通第一个设计的完整流程(附常用命令清单)

本文为数字IC后端设计新手提供了Cadence Innovus工具的完整实战指南。详细介绍了从环境搭建、软件安装与许可证配置,到加载网表、库文件和约束,并逐步完成布局规划、单元布局、时钟树综合、布线及物理验证的核心使用流程。文章最后附有常用Tcl命令清单,帮助初学者快速上手并跑通第一个设计。

vscode5coder的博客 1235

L2正则化与Ridge回归的关系

非常感谢您提供的详细任务描述和要求。我会尽力按照您的指引来撰写这篇技术博客文章。 L2正则化与Ridge回归的关系 1. 背景介绍 机器学习模型在训练过程中容易出现过拟合的问题,这会导致模型在训练集上表现很好但在测试集上泛化性能较差。为了解决这一问题,正则化技术应运而生。其中

AGI×大数据,开启智能时代的认知跃迁;解码AGI,赋能数据驱动的智能革命。 656

HashMap和HashSet

HashMap和HashSet的区别与联系

m0_67683346的博客 6140

Python跨年烟花(完整代码)

Python语言实现跨年烟花动画的完整代码。

AIGC 3万+

哈希表(Hash Table)是一种通过哈希函数进行快速查找的数据结构,常用实现有HashMap和HashSet

HashMap的底层实现是数组和链表,数组里面存放的是链表的头节点,每个节点存储的是一个键值对,通过计算键的哈希值来确定这个键值对在数组中的位置,然后再通过链表来解决哈希冲突的问题。hashmap的扩容机制是指当hashmap中的元素个数超过数组长度乘以负载因子时,就会重新分配一个更大的数组,并将原来的元素重新计算哈希值并插入到新的数组中。HashMap 的扩容机制是指当 HashMap 中的元素个数超过数组长度乘以负载因子时,就会重新分配一个更大的数组,并将原来的元素重新计算哈希值并插入到新的数组中。

[Blog][Domain] programb.blog.youkuaiyun.com 1074

Open3D (C++) 计算点云坐标最值及其索引

使用C++版本的Open3D计算点云最值点及其对应的下标索引

点云侠的博客 727

FM20S用户手册-PL端案例开发手册

为便于演示,本次操作直接将led_flash案例中的 "led_flash_fm20s\constraints\constrs_1Ved_flash_fm20s.fdc"文件全部代码拷贝至编辑 界面,并点击保存按钮。引脚约束请查看案例工程"led_flash\project\led_flash_fm20s\constraints\constrs_1\"目 录下的led_flash_fm20sfdc。(4)弹出如下界面,在界面配置Select device forthe project"内容,如下图。

创龙,嵌入式一体化解决方案商 399

hashset、hashmap、散列表数据结构(哈希表)它们之间的联系

散列表数据结构(哈希表) 散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接进行访问的数据结构。 也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。 HashMap底层就是散列表数据结构,即数组和链表的结合体, 底层是一个数组结构,数组中的每一项又是一个链表。这样做有什么好处呢? 数组能够提供对元素的快速访问但不易于扩展(如果不知道元素脚标,还得进行遍历查找),链表易于扩展但不能对其元素进行快速

haha_66666的博客 562

NX二次开发常用函数——从一个坐标系到另一个坐标系的转换(UF_MTX4_csys_to_csys )相同体坐标转化

本文介绍了在项目开发中实现坐标转换的方法,重点解析了两个关键函数UF_MTX4_csys_to_csys和UF_MTX4_vec3_multiply的使用。通过4个变换矩阵(transform1-4)实现工作坐标系与绝对坐标系之间的相互转换,并以圆柱体为例详细说明了坐标转换的具体实现过程,包括原点、X/Y轴方向的转换处理。相比之前采用的if-else判断方法,这种矩阵转换方式在代码复杂度和运行效率上都更具优势。

weixin_47753171的博客 599

_155_Java_HashSet、HashMap,散列表数据结构(哈希表)

转自https://blog.youkuaiyun.com/a236209186/article/details/51736919,感谢作者的无私分享。  很多开发者,初学者都知道HashSet无序,不可重复,线程非同步。底层是哈希表结构。 但它是怎么做到的?什么是散列表数据结构(哈希表)?有什么特性?都清楚吗?不清楚继续往下看。 它是这样做到的: 先来看HashSet的源码,首先看默认构造器: [ja...

poiuyppp的博客 134

20.Labview使用子vi进行VI间的数据传递详解(模块化)

子VI(模块化)题主在题注中也提到, 子VI的建立只是为了减少程序的负担,那么我们当使用子VI的时候首先就要明确什么是子VI?什么样的程序可以作为子VI?首先我们来看NI官方给的定义,具体说明如下图所示:题主已经将重点画出来了,在官方的解释中将子VI定义为了模块化,大家可以看到创建模块化子VI的特点在于减少代码重复工作优化了程序的框架结构。把一个代码打包,需要的时候就拎出来让他处理处理,不需要的时候就让它躺在我的列表里就行了也不占用运行空间。那么创建子VI需要需要注意以下几点:①任何一个程序。

关注我,更多技术问题欢迎私聊~ 3515

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Java中哈希表的实现

java哈希表

qq_45846957的博客 1013

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

深入浅出 散列表 (哈希表) 带实现

定义 散列表也称为哈希表,是我们一种重要的数据结构,是一种用于以常数平均时间执行插入、删除和查找的技术。维基百科上对散列表的定义如下: 散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表 意思是...

全村最靓的仔 475

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

哈希表相关知识

哈希表(Hash Table),也叫散列表,是一种根据关键码值(Key value)而直接进行访问的数据结构。

2301_81718046的博客 1024

数据结构08(Java)-- 哈希表(HashMap和HashSet)有序表(TreeMap和TreeSet)的简单介绍

本文介绍了哈希表和有序表的基本概念及其实现。哈希表通过哈希函数实现快速查找(O(1)时间复杂度),包括键值对存储的HashMap和去重集合HashSet。有序表则按key顺序组织数据,提供O(logN)复杂度的操作,包括TreeMap和TreeSet。文章还通过代码示例展示了它们的常用方法,并对比了它们的区别。最后强调非基础类型存储时需提供比较器来保证有序性。适合数据结构初学者参考学习。

2301_81189772的博客 485

JAVA语法5:哈希表

1.数组哈希表根据关键码的值而直接进行访问的数据结构。哈希表的三种数据结构:1.数组 2.set集合 3.map映射1.数组。

ohnoooo9的博客 932

Day14 学习java(散列表和泛型)

Set HashSet使用 public class Collection_01 { public static void main(String[] args) { HashSet set = new HashSet(); set.add(1); set.add("abc"); System.out.println(set); set.remove("abc"); for (Object o : set) { System.out.println(o); } }

Qiyesama1314的博客 213

黑马程序员 HashSet、HashMap,散列表数据结构

HashSet: 很多开发者,初学者都知道HashSet无序,不可重复,线程非同步。底层是哈希表结构。 但它是怎么做到的?什么是散列表数据结构(哈希表)?有什么特性?都清楚吗?不清楚继续往下看。 它是这样做到的: 先来看HashSet的源码,首先看默认构造器: [java] view plaincopyprint? public Ha

u010267179的专栏 537

数据结构大纲(java版)

一、线性表  线性表是由n个元素组成的线性序列。是一种动态数据结构,它的表长可以改变。主要由数组和链表两种。 1、数组  数组是一种存储单元连续,用来存储固定大小元素的线性表。java中对应的集合:ArrayList。(java中Array是定长数组,Arraylist是动态数组。) 2、链表  链表是线性表的链接表示,又分单向链表和双向链表,是在物理存储单元上非连续、非顺序的存储结构,数据元素...

Cherils的博客 407
上一篇: 带你实现一个简单的MyApacheTomcat,迷你并发服务器
flowciyu
博客等级 码龄15年 65粉丝 23原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值