Java 1.8 HashMap实现(译注)
来源:互联网 发布:ubuntu hadoop2 编辑:程序博客网 时间:2024/06/07 19:08
译者序
作者整个博客只有这一篇文章,而就这一篇文章,却是介绍HashMap与Java中Hash策略的精品。作者从Java 2讲述到Java 8,细数种种变更,并且用数学公式和清晰的思路解释其原理。全文行文流畅,排版规范典雅,有着论文般的美感,就技术博客而言,实乃佳品。配合HashMap源码消化更佳。
因为时间仓促,在翻译的过程中,难免会有错漏,希望多加指正。
Source: How does Java HashMap work?
Author: CodeHiker42
概述
这篇文档阐述了Java中的HashMap,从早期版本一直到基于Oracle的JDK和OpenJDK的Java 7,8中的实现原理。在文档中,所有引用的源码都来自于Oracle JDK和OpenJDK——这两者在纯粹的Java SDK实现上是完全相同的。我希望这篇文档能够帮助各位到开发者,甚至是那些从未使用过Java的开发者。因为这些内容与如何设计框架或者库无关,它们更多的针对于如何去以实现语言无关的HashMap。
HashMap是Java集合框架(Java Collection Framework, JCF)中一个基础类,它在1998年12月,加入到Java 2版本中。在此之后,Map接口本身除了在Java 5中引入了泛型以外,再没有发生过明显变化。然而HashMap的实现,则为了提升性能,不断地在改变。
实现HashMap时一个重要的考量,就是如何尽可能地规避哈希碰撞。而HashMap实现变更的路线图,也大多与此相关。
HashMap与HashTable
HashMap和HashTable这两个术语,在此文档中指的都是Java的API。
HashTable在Java出现之初,就已经被引入,而HashMap直到Java 2,才随着JCF出现到人们的视野之中。
HashTable和HashMap一样,也实现了Map接口,因此他们从函数的视角上是等价的。
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
Code No.1 HashTable 与 HashMap的声明
然而,在它们之间,有许多处不同。首先,HashTable是一个线程安全的API,它的方法通过synchronized
关键字进行修饰。尽管并不推荐使用HashTable来开发一个高性能的应用,但是它确实能够保证你的应用线程安全。相反,HashMap并不保证线程安全。因此当你构建一个多线程应用时,请使用ConcurrentHashMap。
而在单线程应用中,HashMap有这个比HashTable更好的性能,这得益于HashMap使用了多种方式来规避哈希碰撞,其中,使用辅助Hash函数是一种著名的方式。在Java 8中,一种更好的方式被用来处理高频碰撞的问题。不过,我们需要记住一点,没有完美的哈希函数。但是即使我们无法创造一个完美的世界,让它变得更好也是值得的。
这里,我想要指出HashTable和HashMap这个两个术语的来源。基本上,他们都可以被看做是一种关联数组,关联数组与数组最大的不同,就是对于每一个数据,关联数组会有一个key与之关联,当使用关联数组时,每个数据都可以通过对应的Key来获取。关联数组有许多别名,比如Map(映射)、Dictionary(字典)和Symbol-Table(符号表)。尽管名字不同,他们的含义都是相同的。
字典和符号表都是非常直观的术语,无须解释它们的行为。映射来自于数学领域。在函数中,一个域(集合)中的值被与另一个域(集合)中的值关联,这种关联关系叫做映射。
*Figure No.1 函数中的映射
因此HashTable和HashMap都是基础的关联数组,哈希指的是一种通过Key来获取数据的过程。
哈希分布和哈希碰撞
对于每个对象X和Y,如果当(且仅当,译者注)X.equals(Y)为false,使得X.hashCode() != Y.hashCode()为true,这样的函数叫做完美Hash函数。下面是完美哈希函数的数学表达.
基于对象中变化的域(字段),我们很容易构造一个完美哈希函数。一个
Boolean
对象有true和false两个值,因此Boolean
对象的Hash值可以通过一个二进制位 bit 表达,即0b0, 0b1。对于一些Number
对象,比如Integer
、Long
、Double
等,他们都可以使用自身原始的值作为Hash值。然而,想要构造这样的完美哈希函数,我们需要无限的内存大小,这种假设显然是不可能的。而且,即时我们能够为每个POJO(Plain Ordinary Java Object)或者String对象构造一个理论上不会有冲突的哈希函数,但是hashCode()函数的返回值是int型。根据鸽笼理论,当我们的对象超过这里还有一个点需要我们考虑。我们是否可以在某些限制下,通过允许哈希碰撞来节省内存?这往往是一个提升总体性能不错的方式。许多关联数组的实现,包括HashMap,使用了大小为M的桶来储存
- 1
- 1
Code No.2 获取hash桶索引的方式
因此,当一个对象的插入HashMap,发生哈希冲突的概率是
Figure No.2 Open Adressing and Seperate Chaning
开放寻址是一种解决哈希冲突的方式,当计算出的桶索引的位置被占据时,通过一定的探索方式,来寻找未被占据的哈希桶(适合数量确定,冲突较少的情况,译者注)。而分离链接则将每一个哈希桶作为一个链表的头结点,当哈希碰撞发生时,仅需在链表中进行储存、查找。
这两种方法都有着同样的最坏时间复杂度
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
Code No.3 Java 7中哈希桶的实现
代码4呈现了put()
使用分离链表实现的方式。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
Code No.4 Java 1.7中HashMap的put()方法的实现
Java 8 HashMap的分离链表
从Java 2到Java 1.7,HashMap在分离链表上的改变并不多,他们的算法基本上是相同的。如果我们假设对象的Hash值服从平均分布,那么获取一个对象需要的次数时间复杂度应该是
数据越多,
使用链表还是树,与一个哈希桶中的元素数目有关。代码5中中展示了Java 8的HashMap在使用树和使用链表之间切换的阈值。当冲突的元素数增加到8时,链表变为树;当减少至6时,树切换为链表。中间有2个缓冲值的原因是避免频繁的切换浪费计算机资源。
- 1
- 2
- 3
- 1
- 2
- 3
Code No.5 Java 8 HashMap中的TREEIFY_THRESHOLD & UNTREEIFY_THRESHOLD
Java 8 HashMap使用Node类替代了Entry类,它们的结构大体相同。一个显著地差别是,Node类具有导出类TreeNode,通过这种继承关系,一个链表很容易被转换成树。
Java 8 HashMap使用的树是红黑树,它的实现基本与JCF中的TreeMap相同。通常,树的有序性通过两个或更多对象比较大小来保证。Java 8 HashMap中的树也通过对象的Hash值(这个hash值与哈希桶索引值不同,索引值在这个hash值的基础上对桶大小M取模,译者注)作为对象的排序键。因为使用Hash值作为排序键打破了Total Ordering(可以理解为数学中的小于等于关系,译者注),因此这里有一个tieBreakOrder()方法来处理这个问题。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
Code No.6 Java 8中的Node类
Hash桶动态扩容
小数目的哈希桶可以有效的利用内存,但是会产生更高概率的哈希碰撞,最终损失性能。因此,HashMap会在数据量达到一定大小时,将哈希桶的数量扩充到两倍。当哈希桶的数量变为两倍后,
哈希桶的默认数量是16,最大值是
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
Code No.7 Java 1.7中的哈希桶扩容
确定是否需要对桶进行扩展的临界值是
因为在临界点的扩容会导致所有数据重新插入,那么从一个默认的HashMap一直扩容到当前包含有N个元素的HashMap的消耗,也就是数据的插入次数,可以大致估算出。(原文公式不严格,没有给出上下界,因此没有评估意义。此处公式和结论由译者给出,译者注)
考虑N处于两个区间
当我们向HashMap插入大量数据,而没有指定一个合适的初始桶的数量时,它将会进行至少额外的1N次插入,至多为2N插入。这意味着,如果在一开始就指定了合理的桶的数量,性能将提升1~2倍。
当扩容时,还有另一个问题需要考虑。因为哈希桶的大小M总是
这也是HashMap使用辅助哈希函数的原因。
辅助哈希函数
使用辅助哈希函数的目的是通过改变初始的哈希值,降低发生哈希冲突的概率。辅助哈希函数从JDK 1.4开始被引入,但是Java 5使用了与JDK 1.4中不同的实现。这种实现方式一直延续到了Java 1.7。Java 8使用了比早期版本(Java 5 - Java 8)更为简单的方式来实现。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
Code No.8 Java7 HashMap中的辅助哈希函数
Java 8使用了更为简洁的方式,仅仅是将哈希值的高位与低位混合。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
Code No.9 Java8 HashMap中的辅助哈希函数
我认为,Java 8使用了更简单的方式有两个原因。第一,Java 8引入了树来解决较多哈希冲突的问题;第二,目前哈希函数的设计已经能够很好地避免冲突,因此用一个简单的版本也能够处理冲突的问题。
概念上讲,哈希值的索引通过index = X.hashCode() % M计算。但是M总是2的整数次幂。因此取模操作可以通过一系列性能更高的按位操作符,比如AND, XOR, SHIFT来完成(在程序中,使用 hash(X) & (M - 1)优化性能,其中hash是辅助哈希函数,译者注)。
String对象的Hash函数
String对象的Hash函数的时间开销与String值的长度成正比。在JDK 1.1中,为了提升String类的hashCode的性能,在计算时并没有逐字符进行计算。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Code No.10 JDK 1.1中String类的hashCode函数
正如我们猜测的那样,这会导致一个严峻的问题,尤其是在处理Web URL的时候。因此它很快被丢弃了,一个更加稳定的版本被推出,一直使用到Java 8也没有改变。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Code No.11
代码11展示了hashCode实现。使用秦九韶算法(原文是Horner算法,译者注)来计算。秦九韶算法将一个多项式分解为多个单项式,使之更加容易计算。代码11中的公式可以被如下展开:
使用31的有两个原因。首先31是一个质数;乘31可以被非常快的计算。因为
Java7中另一种String对象Hash函数的实现
从JDK 7u7到 7u25,用户可以通过激活一个特殊操作,使得当HashMap中的超过特定数量时,其中的String对象的哈希值使用一个特殊的哈希函数来计算。这个操作仅当在启动JVM时进行特殊的设置后才生效。JDK 7u40以后,这个操作被移除。因此Java 8中也不存在这样的操作。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
Code No.12 additional hash function for String objects in Java7
这个选项叫做jdk.map.althashing.threshold,使用的函数名为sun.misc.Hashing.stringHash32(),它的算法基于MurMur哈希。使用MurMur的原因也是为了避免哈希冲突。但是它有一个副作用,MurMur需要sum.misc.Hashing.randomHashSeed()产生的哈希种子。这个方法使用Romdum.nextInt()实现。Rondom.nextInt()使用AtomicLong
,它的操作是CAS的(Compare And Swap)。这个CAS操作当有个CPU核心时,会存在许多性能问题。因此,这个替代函数在多核处理器中表现出了糟糕的性能。因此JDK 7u40抛弃了这个函数,Java 8中也没有包含。
总结
这篇文档阐述了从早期版本到现在HashMap的内部实现。HashMap使用分离链表和辅助哈希函数解决哈希冲突问题。Java 8引入了平衡树在一定场合下代理链表进行优化。这篇文档也阐述了为什么String哈希函数使用31这个数字。
HashMap从最早期的阶段开始,进行了不断的改进提升。其中辅助Hash函数的在1.4中的引入和平衡树在Java 8中的引入尤为典型。
有许多很快就消失了的方法,比如Java 7中的MurMur哈希函数,尽管被期望带来更好的时间效率,但是它们很难达成目标。
就在刚刚的这个一个Http请求发生的瞬间,有许多HashMap的实例被创建了。仅仅一秒,它们就可能已经成为了GC的目标。随着内存容量的增长,以内存为中心的应用也不断的增多,其中大量的数据大多被储存到一个单独的HashMap之中。
此时此刻,我们无法的得知HashMap在Java 9、Java 10中的变化,但是有一点很明显,HashMap会随着计算环境的发展不断改变。
- Java 1.8 HashMap实现(译注)
- Java 1.8 HashMap实现(译注)
- java 1.8 hashMap的实现原理
- hash算法实现-(java-hashmap)
- HashMap(Java 7)的实现原理
- 揭秘 HashMap 实现原理(Java 8)
- Java HashMap实现详解
- Java HashMap实现原理
- Java HashMap实现详解
- Java HashMap实现详解
- Java HashMap实现详解
- Java HashMap实现详解
- Java HashMap实现详解
- Java HashMap实现详解
- Java HashMap实现详解
- Java HashMap实现详解
- 实现Java hashmap
- Java HashMap实现原理
- 十年经验教你如何学习嵌入式系统
- ajax入门 不要畏惧 很简单 进了门一切都好学多了
- Java自定义带错误码的异常
- 安卓java代码动态实现Selector
- linux串口驱动分析
- Java 1.8 HashMap实现(译注)
- Java Web页面注销登录结束整个会话
- HTML5服务器推送消息的各种解决办法
- 关于IO流文件遍历中过滤功能实现
- C3P0的三种配置方式以及基本配置项详解
- 直接修改别人jar包里面的class文件 工具:jclasslib
- Ecstore中finder”查看”下的分页实现
- H264文件封装MP4文件
- windows下ftp计划任务上传失败的一些问题