Java HashMap工作原理及实现
1. 概述
从本文你可以学习到:
- 什么时候会使用HashMap?他有什么特点?
- 你知道HashMap的工作原理吗?
- 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
- 你知道hash的实现吗?为什么要这样实现?
- 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
当我们执行下面的操作时:
1 | HashMap<String, Integer> map = new HashMap<String, Integer>(); |
运行结果是
政治: 5
生物: 7
历史: 4
数学: 2
化学: 8
语文: 1
英语: 3
地理: 6
发生了什么呢?下面是一个大致的结构,希望我们对HashMap的结构有一个感性的认识:
在官方文档中是这样描述HashMap的:
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.
几个关键的信息:基于Map接口实现、允许null键/值、非同步、不保证有序(比如插入的顺序)、也不保证序不随时间变化。
2. 两个重要的参数
在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)
- Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
- Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
简单的说,Capacity就是buckets的数目,Load factor就是buckets填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity
设置过大,也不要把load factor
设置过小。当bucket填充的数目(即hashmap中元素的个数)大于capacity*load factor
时就需要调整buckets的数目为当前的2倍。
3. put函数的实现
put函数大致的思路为:
- 对key的hashCode()做hash,然后再计算index;
- 如果没碰撞直接放到bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity),就要resize。
具体代码的实现如下:
1 |
|
4. get函数的实现
在理解了put之后,get就很简单了。大致思路如下:
- bucket里的第一个节点,直接命中;
- 如果有冲突,则通过key.equals(k)去查找对应的entry
若为树,则在树中通过key.equals(k)查找,O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n)。
具体代码的实现如下:
1 | public V get(Object key) { |
5. hash函数的实现
在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示:
在对hashCode()计算hash时具体实现是这样的:
1 | static final int hash(Object key) { |
可以看到这个函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或。其中代码注释是这样写的:
Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
在设计hash函数时,因为目前的table长度n为2的幂,而计算下标的时候,是这样实现的(使用&
位操作,而非%
求余):
1 | (n - 1) & hash |
设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在n - 1为15(0x1111)时,其实散列真正生效的只是低4bit的有效位,当然容易碰撞了。
因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)
的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
如果还是产生了频繁的碰撞,会发生什么问题呢?作者注释说,他们使用树来处理频繁的碰撞(we use trees to handle large sets of collisions in bins),在JEP-180中,描述了这个问题:
Improve the performance of java.util.HashMap under high hash-collision conditions by using balanced trees rather than linked lists to store map entries. Implement the same improvement in the LinkedHashMap class.
之前已经提过,在获取HashMap的元素时,基本分两步:
- 首先根据hashCode()做hash,然后确定bucket的index;
- 如果bucket的节点的key不是我们需要的,则通过keys.equals()在链中找。
在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
因此在Java 8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题,在Java 8:HashMap的性能提升一文中有性能测试的结果。
6. resize的实现
当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。resize的注释是这样描述的:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
大致意思就是说,当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:
因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
下面是代码的具体实现:
1 | final Node<K,V>[] resize() { |
7. 总结
我们现在可以回答开始的几个问题,加深对HashMap的理解:
1. 什么时候会使用HashMap?他有什么特点?
是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。
2. 你知道HashMap的工作原理吗?
通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
4. 你知道hash的实现吗?为什么要这样实现?
在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)
,主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。
关于Java集合的小抄中是这样描述的:
以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。
插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。
JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。
在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。
当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。
取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。
iterator()时顺着哈希桶数组来遍历,看起来是个乱序
参考资料
HashMap的工作原理
Java 8:HashMap的性能提升
JEP 180: Handle Frequent HashMap Collisions with Balanced Trees
ConurrentHashMap和Hashtable的区别
HashMap和Hashtable的区别
算法学习笔记
算法虐我千百遍,我待算法如初恋
这里的内容是我学习算法过程的一些记录,希望能一直坚持下去。
学习方法
- 把所有经典算法写一遍
- 看算法有关源码
- 加入算法学习社区,相互鼓励学习(加我vx:tiger-ran, 备注入群理由, 拉你入群)
- 看经典书籍
- 刷题
基本数据结构和算法
这些算法全部自己敲一遍:
链表
- 链表
- 双向链表
哈希表/散列表 (Hash Table)
- 散列函数
- 碰撞解决
字符串算法
- 排序
- 查找
- BF算法
- KMP算法
- BM算法
- 正则表达式
- 数据压缩
二叉树
- 二叉树
- 二叉查找树
- 伸展树(splay tree 分裂树)
- 平衡二叉树AVL
- 红黑树
- B树,B+,B*
- R树
- Trie树(前缀树)
- 后缀树
- 最优二叉树(赫夫曼树)
- 二叉堆 (大根堆,小根堆)
- 二项树
- 二项堆
- 斐波那契堆(Fibonacci Heap)
图的算法
- 图的存储结构和基本操作(建立,遍历,删除节点,添加节点)
- 最小生成树
- 拓扑排序
- 关键路径
- 最短路径: Floyd,Dijkstra,bellman-ford,spfa
排序算法
交换排序算法
- 冒泡排序
- 插入排序
- 选择排序
- 希尔排序
- 快排
- 归并排序
- 堆排序
线性排序算法
- 桶排序
查找算法
- 顺序表查找:顺序查找
- 有序表查找:二分查找
- 分块查找: 块内无序,块之间有序;可以先二分查找定位到块,然后再到
块
中顺序查找 - 动态查找: 二叉排序树,AVL树,B- ,B+ (这里之所以叫
动态查找表
,是因为表结构是查找的过程中动态生成的) - 哈希表: O(1)
15个经典基础算法
- Hash
- 快速排序
- 快递选择SELECT
- BFS/DFS (广度/深度优先遍历)
- 红黑树 (一种自平衡的
二叉查找树
) - KMP 字符串匹配算法
- DP (动态规划 dynamic programming)
- A*寻路算法: 求解最短路径
- Dijkstra:最短路径算法 (八卦下:Dijkstra是荷兰的计算机科学家,提出”信号量和PV原语“,”解决哲学家就餐问题”,”死锁“也是它提出来的)
- 遗传算法
- 启发式搜索
- 图像特征提取之SIFT算法
- 傅立叶变换
- SPFA(shortest path faster algorithm) 单元最短路径算法
海量数据处理
- Hash映射/分而治之
- Bitmap
- Bloom filter(布隆过滤器)
- Trie树
- 数据库索引
- 倒排索引(Inverted Index)
- 双层桶划分
- 外排序
- simhash算法
- 分布处理之Mapreduce
算法设计思想
- 迭代法
- 穷举搜索法
- 递推法
- 动态规划
- 贪心算法
- 回溯
- 分治算法
算法问题选编
这是一个算法题目合集,题目是我从网络和书籍之中整理而来,部分题目已经做了思路整理。问题分类包括:
- 字符串
- 堆和栈
- 链表
- 数值问题
- 数组和数列问题
- 矩阵问题
- 二叉树
- 图
- 海量数据处理
- 智力思维训练
- 系统设计
还有部分来自算法网站和书籍:
- 九度OJ
- leetcode
- 剑指offer
开源项目中的算法
- YYCache
- cocos2d-objc
- …
推荐阅读
刷题必备
- 《剑指offer》
- 《编程之美》
- 《编程之法:面试和算法心得》
- 《算法谜题》 都是思维题
基础
- 《编程珠玑》Programming Pearls
- 《编程珠玑(续)》
- 《数据结构与算法分析》
- 《Algorithms》 这本近千页的书只有6章,其中四章分别是排序,查找,图,字符串,足见介绍细致
算法设计
- 《算法设计与分析基础》
- 《算法引论》 告诉你如何创造算法 断货
- 《Algorithm Design Manual》算法设计手册 红皮书
- 《算法导论》 是一本对算法介绍比较全面的经典书籍
- 《Algorithms on Strings,Trees and Sequences》
- 《Advanced Data Structures》 各种诡异高级的数据结构和算法 如元胞自动机、斐波纳契堆、线段树 600块
延伸阅读
- 《深入理解计算机系统》
- 《TCP/IP详解三卷》
- 《UNIX网络编程二卷》
- 《UNIX环境高级编程:第2版》
- 《The practice of programming》 Brian Kernighan和Rob Pike
- 《writing efficient programs》 优化
- 《The science of programming》 证明代码段的正确性 800块一本
参考链接和学习网站
July 博客
- 《数学建模十大经典算法》
- 《数据挖掘领域十大经典算法》
- 《十道海量数据处理面试题》
- 《数字图像处理领域的二十四个经典算法》
- 《精选微软等公司经典的算法面试100题》
- The-Art-Of-Programming-By-July
- 微软面试100题
- 程序员编程艺术
基本算法演示
http://sjjg.js.zwu.edu.cn/SFXX/sf1/sfys.html
http://www.cs.usfca.edu/~galles/visualization/Algorithms.html
编程网站
其它
高级数据结构和算法 北大教授张铭老师在coursera上的课程。完成这门课之时,你将掌握多维数组、广义表、Trie树、AVL树、伸展树等高级数据结构,并结合内排序、外排序、检索、索引有关的算法,高效地解决现实生活中一些比较复杂的应用问题。当然coursera上也还有很多其它算法方面的视频课程。
算法设计与分析 Design and Analysis of Algorithms 由北大教授Wanling Qu在coursera讲授的一门算法课程。首先介绍一些与算法有关的基础知识,然后阐述经典的算法设计思想和分析技术,主要涉及的算法设计技术是:分治策略、动态规划、贪心法、回溯与分支限界等。每个视频都配有相应的讲义(pdf文件)以便阅读和复习。
OI Wiki 主要内容是 OI / ACM-ICPC 相关的知识整理。
Centos7一键加固脚本
脚本使用
1 | sh ./init_centos7.sh |
init_centos7.sh 脚本如下,请直接粘贴到linux 文本编辑器中,使用window 文本编辑器会报错
1 | #!/usr/bin/env bash |
一辈子很短,努力的做好两件事就好;
第一件事是热爱生活,好好的去爱身边的人;
第二件事是努力学习,在工作中取得不一样的成绩,实现自己的价值,而不是仅仅为了赚钱;
Centos8 本地无法登录远程可以登录的问题解决
linux本机root账户无法登录(root和密码无误的情况下也无法登录,但是用远程ssh软件可以登录)
查看系统中pam_limits.so文件是否存在
[root@server181 ~]# find / -name pam_limits.so
/usr/lib64/security/pam_limits.so
发现pam_limits.so文件没有在 指定的目录下
在vim /etc/pam.d/login
增加
session required /usr/lib64/security/pam_limits.so
Failed to Start LSB Bring Up Down错误解决方法
Failed to start LSB: Bring up/down错误解决方法
在使用centos7系统时,有时候需要分配多个IP地址,这就涉及到修改网卡配置,但是在修改完网卡配置时,重启网络服务时会出现“Failed to start LSB: Bring up/down”网络报错,这个应该应该怎么解决呢?
其实使用提示命令systemctl status network.service进行查看可以发现错误行:
1 | [[email protected] ~]# systemctl status network.service |
解决方法如下:
第一种方法:修改MAC地址
这样造成的原因是配置文件中MAC与当前网卡MAC不一致,只需要修改一下配置文件即可。
1、用ip addr show命令查看当前MAC地址
1 | [root@localhost network-scripts]# ip addr show |
2、修改/etc/sysconfig/network-scripts/下以ifcfg开头的网络链接文件
例如
1 | vim /etc/sysconfig/network-scripts/ifcfg-eth1 |
将HWADDR=”00:0c:29:7f:76:e8” 改为HWADDR=”00:0c:29:ec:12:34”
3、重启网络,这样状态既可正常。
1 | systemctl restart network.servic |
Centos安装docker显示 No Package Docker-Ce Available
查看当前系统内核
查看方式
1 | uname -r |
显示如下
1 | [root@localhost home]# uname -r |
重要提示: docker内核版本必须是3.10+以上的版本
- 卸载老版本的 docker 及其相关依赖
1
yum remove docker docker-common container-selinux docker-selinux docker-engine
1 | [root@localhost home]# yum remove docker docker-common container-selinux docker-selinux docker-engine |
2.更新yum
1 | yum update |
之后需要一段时间更新
3.安装 yum-utils,它提供了 yum-config-manager,可用来管理yum源
1 | yum install -y yum-utils |
1 | [root@localhost home]# yum install -y yum-utils |
4. 添加yum源
1
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
1 | [root@localhost home]# yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo |
5. 更新索引
1
yum makecache fast
1 | [root@localhost home]# yum makecache fast |
6. 安装 docker-ce
1
yum install -y docker-ce
之后需要一段时间安装
7. 启动 docker
systemctl start docker
8. 验证是否安装成功
1
docker info
1 | [root@localhost home]# docker info |
安装docker-compose
1 安装epel源
1 | yum install -y epel-release |
2 安装docker-compose
1 | yum install -y docker-compose |
CentOS8安装Docker报错以及解决
CentOS8安装Docker报错
CentOS8安装Docker出现package docker-ce-3:19.03.8-3.el7.x86_64 requires containerd.io >= 1.2.2-3
使用了CentOS8,尝试安装Docker时出现了错误,故及时记录一下。
错误提示
1 | Problem: package docker-ce-3:19.03.8-3.el7.x86_64 requires containerd.io >= 1.2.2-3, but none of the providers can be installed |
问题分析
centos8默认使用podman代替docker,所以需要containerd.io,那我们就安装一下就好了
解决方法
安装containerd.io即可
1 | yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/containerd.io-1.2.6-3.3.fc30.x86_64.rpm |
然后继续安装Docker
docker安装的一些依赖
1 | yum install -y yum-utils device-mapper-persistent-data lvm2 |
增加yum仓库
1 | yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo |
1 | yum install -y docker-ce |
若此步报错如下
1 | (尝试在命令行中添加 '--allowerasing' 来替换冲突的软件包 或 '--skip-broken' 来跳过无法安装的软件包 或 '--nobest' 来不只使用最佳选择的软件包) |
尝试执行
1 | yum install -y --allowerasing docker-ce |
启动docker
1 | systemctl start docker |
设置开机启动
1 | systemctl enable docker |