哈希存储 java_Java容器系列之HashMap的存储

545a116aecd9

Java容器系列之HashMap

概要

本文将结合Java源码总结HashMap的存储结构及其扩容策略,并根据这些特点给出使用HashMap的最佳实践。

本文不再介绍HashMap的基本使用,有需要的请先学习下Java容器的基础知识。

存储结构

HashMap的核心问题是如何保证读写的速度?答案是使用Key对象的Hash值来合理存储对象。我们知道,每个java对象都有其默认的hashCode()方法,也就是每个对象都有一个int值与之对应。那么如何利用这个int值来快速存储对象呢?

1.按照hash值找坑位

假设定义了一个长度为8的HashMap,那么HashMap会创建一个长度为8的数组作为容器来存放键值对(在HashMap内部使用了Node对象来存储)。也就是说,这个数组的元素是Node对象。那么,此时有8个坑位可以用来放置元素。我们按照Java的数组index标号,也就是从0到7分别标号。

Key对象的Hash值我们是能够得到的,此时如果把这个Hash值来对8取模,自然是从0到7的一个分布了。故根据Key对象的Hash值,我们可以快速找到该Key放在了数组的哪个坑位,避免的数组的轮询。

总结下,坑位的取得方式,简单的说就是先得到Key的HashCode,然后把这个值对HashMap的长度取模。

此处有一个问题,如果两个不同的Hash值,取模的结果相等怎么办?比如: 如果Key1的hash值是7,Key2的hash值是15,此时它们对8取模,都是得到7,不可能把这两个对象放在一个坑位。

2.当不同的Key得到同一个坑位后,按照链表或者红黑树存储

JDK8之前,使用了链表来存储hash取模后一样的元素。而JDK8开始使用了链表和红黑树相结合的方式来存储。

链表存储

查看Node的定义,可以看到它包含了一个next属性。

static class Node implements Map.Entry {

final int hash;

final K key;

V value;

Node next;

......

}

正是利用这个next,实现了链表的存储。此时,从HashMap取元素的时候,首先找到坑位,然后遍历坑位中的链表,逐一判断Key对象的hashCode是否相等,而且Key对象的equals方法是否返回true。当且仅当hashCode相等,而且equals方法返回true时,才认为找到了key对应的元素,并返回该Node对象中包含的value对象。

总结下,一个坑位下挂载了若干个元素,这些元素有以下特点。

1)同一坑位下的链表中,各个元素的Key对象Hash值取模后的值都是一样的。

2)链表的先后顺序按照进入HashMap的时间顺序排列,新元素被放入坑位,并且其next指向原来在坑位中的元素。

3)这个顺序是可能变化的,比如扩容的时候。这样也就不难理解为什么HashMap无法保证元素的排序了。

显然,这样有个缺点,就是如果这个链表很长,那边取元素的性能会随着链表的拉长而变差,而且是越来越差。所以,JDK8开始,引入了红黑树的存储方式。当某个坑位下的链表的长度小于8的时候,还是链表存储;否则,变换成红黑树存储。

红黑树存储

红黑树能够保证存取元素的速度不会随着元素数量的增加而迅速恶化(时间复杂度为lg(n))。具体这里不展开讲,红黑树的资料还是很多的。

查看JDK8的HashMap源码,其putVal方法如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node[] tab; Node p; int n, i;

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null) // 取模使用的是长度减1再跟hash位与操作

tab[i] = newNode(hash, key, value, null);

else {

Node e; K k;

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

else if (p instanceof TreeNode)

e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

else {

for (int binCount = 0; ; ++binCount) {

if ((e = p.next) == null) {

p.next = newNode(hash, key, value, null);

if (binCount >= TREEIFY_THRESHOLD - 1) //达到8则转换成树

treeifyBin(tab, hash);// 链表转换成红黑树

break;

}

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

扩容策略

HashMap在使用的时候并没有人会关注其大小,看上去这个容器是无限大小的,反正有东西就往里面放,至于里面放了多少,还能放多少,没人关心。这完全得益于其自动化的扩容机制。HashMap内部定义了一套扩容的机制,以应对元素的扩张。

1.HashMap的扩容阈值

当往HashMap中放入元素的时候,如果HashMap中的元素个数达到某个阈值时,会触发扩容操作。这个阈值由loadFactor属性控制的,这个值是个小数(默认值是0.75),阈值等于map的整体容量乘以loadFactor。如此做的目的是确保坑位的量足够,让元素能够足够的分散。扩容后的容量一般是翻倍,而且容量都是2的幂,比如2,4,8,16,32,64.......这样做的目的是为了方便取模的操作(取模的操作很巧妙,会再开一文)。

2.HashMap的扩容方法

扩容的操作通常是增加容量后,重新安置各个元素。扩容后,元素的放置都重新打乱了,等于是重新生成了一次HashMap。所以,尽量减少扩容是比较明智的。

最佳实践

使用HashMap的时候,如果能够预估容器的大小,那么在初始化的时候就指定size的大小。这样可以尽量减少容器的扩容操作,提高程序运行效率。下例中,我们在初始化map的时候,指定了其size为100。如下所示:

Map map = new HashMap(100);

虽然代码中定义的长度是100,但是实际的长度是大于100的最小的2的n次方的值,有点绕口。举例如下:

如果指定了50,那么实际长度是2的6次方,64

如果指定了100,那么实际长度是2的7次方,128

以此类推。。。

2.HashMap不是线程安全的,需要注意多线程的同步问题。特别是可能导致死循环的问题(在HashMap扩容时,多线程的情况下使用HashMap可能会导致死循环)。如果需要保证线程安全,建议使用HashMap+Collections工具类的组合来做,而不是使用Hashtable。如下所示:

Map map = new HashMap();

Map synMap = Collections.synchronizedMap(map);

synMap.put("key1", "value1");

当然,Hashtable也是线程安全的,但是该类的同步控制粒度比较大,跟上面比性能更低些,而且该类也是逐步被废弃的态势在发展,少用吧。

线程安全是消耗性能的,所以能回避线程安全问题就尽量回避,这是上上策。

3.如果还想要保证顺序,可以考虑使用LinkedHashMap来实现。它是在HashMap的基础上增加了一组保存先后顺序的双向链表。可以按照LRU(最不常用的放到最后,越常用的越在前面),或者按照放入容器的顺序排列。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465299.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

你增长的年龄,是因为丢掉了快乐吗?

这是周末,想写一篇文章给自己的一个朋友,试看能否解开他不安的情绪。我朋友是我真的朋友几年前我还是很害羞的我,有一次开技术会上,他公开的介绍了我,我觉得是一种认可,当然,也不会有一个不喜欢…

redhat5中架设DHCP服务器与DHCP中继

作者:奇异果Wickey Email:hkb178149081163.com 实验平台:VMvare 系统:RedHat5 软件包:DHCP-3.0.5 模拟需求分析:(暂无) 拓扑图: 1. 配置DHCPServer IP与GW 2. 安装DHCP…

MCU HardFault问题查找和破解方法

一、HardFault产生原因和常规分析方法二、HardFault解决方法分析三、HardFault回溯的原理四、操作分析流程:1. 心里明白徒手分析法2. CmBacktrace 天龙大法五、总结一、HardFault产生原因和常规分析方法在嵌入式开发中,偶尔会遇到Hard Fault死机的异常&…

ASP.NET Core的身份认证框架IdentityServer4--入门【转】

原文地址 Identity Server 4是IdentityServer的最新版本,它是流行的OpenID Connect和OAuth Framework for .NET,为ASP.NET Core和.NET Core进行了更新和重新设计。在本文中,我们将快速了解IdentityServer 4存在的原因,然后直接进入…

Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

栈是什么?栈有什么作用?首先,栈 (stack) 是一种串列形式的 数据结构。这种数据结构的特点是 后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:栈顶 top) 进行 推入 (push) 和 弹出 (pop) 操作。根据栈…

内核抢占会让内核调度更好吗?

大家好,我是老吴的朋友,这篇文章转发自老吴的公众号。今天要分享的是抢占相关的基础知识。本文以内核抢占为引子,概述一下 Linux 抢占的图景。我尽量避开细节问题和源码分析。什么是内核抢占?别急,咱们慢慢来。先理解抢…

php 解压dat,电脑微信dat文件怎么打开

微信dat转码软件使用操作说明在线解码,各位同学下载软件后,如何操作?如何找到dat文件?如何使用?又有哪些注意事项呢?这里会为大家一一道来。问题1 :如果下载失败怎么办?请先检查网络,软件并没有放在我的网站上,而是放在大厂七…

优雅地用宏实现环形缓冲区

之前写的环行缓冲区文章柔性数组和环形队列之间的故事C语言,环形队列循环缓冲区是嵌入式软件工程师在日常开发过程中的关键组件。多年来,互联网上出现了许多不同的循环缓冲区实现和示例。我非常喜欢这个模块,可以GitHub上找到这个开源的 CBUF…

XP访问Linux共享错误提示

XP访问RHEL5共享错误提示 [rootlocalhost samba]# sestatus SELinux status: enabled SELinuxfs mount: /selinux Current mode: enforcing Mode from config file: enforcing Policy version: 21 …

一个漂亮的电子钟,纪念我们逝去的青春(含软硬件资料)

来源:阿莫论坛,作者:humancn微信公众号:芯片之家(ID:chiphome-dy)公众号不少粉丝,大一大二做的第一个项目,都是电子时钟吧,非常经典的STC89C52DS1302数码管组…

JZOJ 5776. 【NOIP2008模拟】小x游世界树

5776. 【NOIP2008模拟】小x游世界树 (File IO): input:yggdrasil.in output:yggdrasil.out Time Limits: 1500 ms Memory Limits: 262144 KB Detailed Limits Goto ProblemSetDescription 小x得到了一个(不可靠的)小道消息,传说中的神岛阿瓦隆在格陵兰海的某处,据…

SQL Server 2005中的分区表(一):什么是分区表?为什么要用分区表?如何创建分区表?...

如果你的数据库中某一个表中的数据满足以下几个条件,那么你就要考虑创建分区表了。 1、数据库中某个表中的数据很多。很多是什么概念?一万条?两万条?还是十万条、一百万条?这个,我觉得是仁者见仁、智者见智…

java图形界面颜色随机变换,JavaScript实现鼠标移入随机变换颜色

大家好!今天分享一个在 JavaScript中,实现一个鼠标移入可以随机变换颜色。/* 这里定义一下div(块元素)已下span 标签的宽.高.边框线以及边框线的颜色*/span{display: block;width: 80px;height: 80px;border: 1px solid #000000;float: left;}var adocum…

Vscode 用Filter Line看日志,很爽

因为某种原因,我抛弃了Notepad然后一直没有找到一个比较好的日志查看软件,最近发现Vscode里面的这个插件不错,给大家推荐一下。中文详情链接:https://everettjf.github.io/2018/07/03/vscode-extension-filter-line/推荐阅读&…

zblog php 七牛缩略图,zblog中Gravatar头像不显示解决方法

解决zblog博客Gravatar头像不显示方法一第一个,解决zblog博客Gravatar头像不显示解决方法是对其进行修复操作。造成不显示的原因主要是Gravatar头像地址错误。所以,我们需要对头像地址进行更改。1、进入自己的博客后台。2、找到现在使用的主题模板中的&a…

SpringCloud学习--微服务架构

目录 微服务架构快速指南 SOA Dubbo Spring Cloud Dubbo与SpringCloud对比 微服务(Microservice)架构快速指南 什么是软件架构?    软件架构是一个包含各种组织的系统组织,这些组件包括 Web服务器, 应用服务器, 数据库,存储, 通讯层), 它们彼此或和环境存在关系…

工作九年的硬件工程师,想对我们说些什么?

△向上生长, TO BE TO UP. 10万工程师的成长充电站△作者:徐新文,排版:晓宇微信公众号:芯片之家(ID:chiphome-dy)时光荏苒,岁月如梭,转眼就在硬件工程师的岗位上工作了九…

StringBuffer/StringBuilder/String的区别

1、在执行速度上:Stringbuilder->Stringbuffer->String 2、String是字符串常量 Stringbuffer是字符串变量 Stringbuilder是字符串变量 有可能我们会疑惑String怎么是字符串变量。看以下代码: String str adc; str str “ef”&#x…

你知道kernel version的实现原理和细节吗

引言kernel 启动时通常会看到下面第二行信息的内容,它们代表了当前 kernel 的版本、编译工具版本、编译环境等信息。Booting Linux on physical CPU 0x0 Linux version 5.4.124 (funnyfunny) (gcc version 6.5.0 (Linaro GCC 6.5-2018.12)) #30 SMP Sat Sep 11 11:1…

Android 为你的应用程序添加快捷方式【优先级高的快捷方式】

有人会说,快捷方式,不是安装完应用程序后,长按应用程序的ICON然后将它拖到桌面上不就行了吗?没错,这样是一种方法,但这种方法有一个缺点,看图吧: 如上图,如果我们长按桌面…