摘要:
Hashtable与HashMap都是Map族中较为常用的实现,也都是Java Collection Framework的重要成员,它们的本质都是链表数组。本文深入研究JDK源码并从定义,构造,结构,存取等四个方面深入解读了哈希表的底层结构与存储逻辑,并阐述了HashMap中,哈希表与ConcurrentHashMap的三者间的联系与区别。
友情提示:
本文所有关于Hashtable的源码都是基于JDK 1.6 的,不同的JDK版本之间也许会有些许差异,但不影响我们对Hashtable的数据结构,原理等整体的把握和了解。
为了更好地了解Hashtable,建议读者先对HashMap有一个深入的了解,读者可以参考我的博文“Map综述(一):彻头彻尾理解HashMap”进行回顾和温习。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http : //blog.csdn.net/justloveyou_/
一。Hashtable概述
Hashtable和HashMap既是Java Collection Framework的重要成员,也是Map族(如下图所示)的核心成员,二者的底层实现都是一个链表数组,具有寻址容易,插入和删除也容易的特性。一个HashMap几乎可以等价于Hashtable中,除了HashMap的是非线程安全的并且可以接受空键和空值。
二。Hashtable在JDK中的定义
Hashtable实现地图接口,并继承字典抽象类(已过时,新的实现应该实现Map接口而不是扩展此类),其在JDK中的定义为:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 三十
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
与HashMap类似,Hashtable也包括五个成员变量,分别是表数组,Hashtable中条目个数,Hashtable的阈值阈值,Hashtable的负载因子loadFactor和Hashtable结构性修改次数modCount。下面分别给出这五个成员的具体内涵:
-
Entry数组表:一个由Entry对象组成的链表数组,表数组的每个数组成员就是一个链表;
-
输入个数:Hashtable中输入对象的个数;
-
阈值阈值:Hashtable进行扩容的阈值;
-
负载因子loadFactor:在其容量自动增加之前可以达到多满的一种尺度,默认为0.75;
-
结构性修改次数modCount:记录Hashtable生命周期中结构性修改的次数,便于快速失败(所谓快速失败是指其在并发环境中进行迭代操作时,若其他线程对其进行了结构性的修改,这时迭代器能够立马感知到并且立即抛出ConcurrentModificationException的异常,而不是等到迭代完成之后才告诉你(你已经出错了));
三。Hashtable的构造函数
Hashtable一共提供了四个构造函数,其中默认无参的构造函数和参数为Map的构造函数为Java集合框架规范的推荐实现,其余两个构造函数则是Hashtable专门提供的。
1,Hashtable(int initialCapacity,float loadFactor)
该构造函数意在构造一个指定初始容量和指定负载因子的空Hashtable,其源码如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
2,哈希表()
该构造函数意在构造一个具有默认初始容量(11)和默认负载因子(0.75f)的空Hashtable,是Java Collection Framework规范推荐提供的,其源码如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
3,Hashtable(int initialCapacity)
该构造函数意在构造一个指定初始容量和默认负载因子(0.75f)的空哈希表,其源码如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
4,散列表(Map <?extends K,?extends V> t)
该构造函数意构造一个与指定Map具有相同映射的Hashtable,其初始容量不小于11(具体依赖于指定Map的大小),负载因子是0.75f,是Java Collection Framework规范推荐提供的,其源码如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
与HashMap类似,构建一个Hashtable时也需要指定初始容量和负载因子这两个非常重要的参数,它们是影响Hashtable性能的关键因素。其中,容量表示哈希表中的桶的数量(table数组的大小) ,初始容量是创建哈希表时桶的数量;负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
对于Hashtable而言,查找一个元素的平均时间是O(1 + a)(a指的是链的长度,是一个常数)。的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重系统默认负载因子为0.75f,这是时间和空间成本上一种折衷,一般情况下我们是无需修改的。
四。Hashtable的数据结构
我们知道,在Java的中最常用的两种结构是数组和链表,其中,数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易.Hashtable和HashMap中综合了两者的特性,是一种寻址容易,插入和删除也容易的数据结构实际上,哈希表和HashMap的本质上都是一个。链表数组,如下所示:
从上图中,我们可以形象地看出Hashtable的底层实现还是数组,只是数组中存放的元素是输入对象,而输入对象是一种典型链状结构,定义如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
入口为Hashtable中的内部类,实现了Map.Entry的接口,是个典型的四元组,包含了键键,值值,指向下一个节点的指针旁边,以及关键的哈希值四个属性。事实上,进入是构成哈希表的基石,是哈希表所存储的元素的具体形式。
五。Hashtable的快速存取
我们知道,在HashMap中,最常用的两个操作就是:put(Key,Value)和get(Key)。同样地,这两个操作也是Hashtable最常用的两个操作。下面我们结合JDK源码看Hashtable的存取实现。
1,Hashtable的存储实现
在Hashtable中,键值对的存储是也是通过put(key,vlaue)方法来实现的,不同于HashMap的是,其放操作是线程安全的,源码如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 三十
- 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
通过上述源码我们可以看出,哈希表与HashMap中保存数据的过程基本相同:首先,计算密钥的散列值并确定ķ/ V对要插入的桶位;其次,查找该桶位中是否存在具有相同的关键的ķ/ V对,若存在则覆盖直接对应的值值,否则将该节点(K / V)保存在桶中的链表的链头位置(最先保存的元素放在链尾)。当然,若该桶位是空的,则直接保存特别地,在一些细节上,哈希表与HashMap中还是有一定的差别的:
-
哈希表不同于HashMap的,前者既不允许键为空,也不允许价值为空;
-
HashMap的中用于定位桶位的密钥的哈希值的计算过程要比Hashtable的复杂一点,没有Hashtable的如此简单,直接;
-
在HashMap中的插入ķ/ V对的过程中,总是先插入后检查是否需要扩容;而哈希表则是先检查是否需要扩容后插入;
-
哈希表不同于HashMap的,前者的把操作是线程安全的。
2,Hashtable的重哈希操作
重哈希过程主要是一个重新计算原哈希表中的元素在新表数组中的位置并进行复制处理的过程,我们直接看其源码:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
特别需要注意的是,在重哈希的过程中,原属于同一个桶中的对象可能会被分到不同的桶,因为Hashtable的容量发生了变化,那么(e.hash&0x7FFFFFFF)%newCapacity的值也会发生相应的变化。退一步说,如果重哈希后原属于一个桶中的条目对象仍属于同一桶,那么重哈希也就失去了意义。
3,Hashtable的读取实现
相对于哈希表的存储操作而言,读取就显得比较简单了。因为哈希表只需通过键的哈希值定位到表数组的某个特定的桶,然后查找并返回该键对应的值即可,源码如下:
- 1
- 2
- 3
- 4
- 五
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
在这里能够根据键快速的取到值,除了和哈希表的数据结构密不可分外,还和入学有莫大的关系。在前面就已经提到过,哈希表在存储过程中并没有将键,值分开来存在,而是当做一个整体Entry对象(四元组)来处理的。可以看到,在Entry对象中,value的地位要比key低一些,相当于是key的附属。在读取细节上,Hashtable与HashMap中的主要差别如下:
-
不同于HashMap中,哈希表的读取操作是同步的;
-
在HashMap中,若读取到的值为NULL,则存在如下两种可能:该键对应的值就是null或者HashMap中不存在该键;而在Hashtable中却只有一种可能:Hashtable中不存在含有该键的输入造成这种差别的原因正是二者对重点和值的限制的不同:HashMap中最多允许一个键为空,但允许多个值值为NULL; Hashtable中既不允许空的关键,也不允许空的值。
六。HashMap,Hashtable与ConcurrentHashMap的联系与区别
1,哈希表与HashMap中的联系与区别
(1)。HashMap的哈希表和的实现模板不同:虽然二者都实现了地图接口,但哈希表继承于字典类,而HashMap的是继承于AbstractMap.Dictionary是是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于地图接口的骨干实现,它以最大限度地减少实现此接口所需的工作。
(2)。HashMap的哈希表和对键值的限制不同:HashMap中可以允许存在一个为空的键和任意个为空的值,但是哈希表中的键和值都不允许为空。
(3)。HashMap的哈希表和的线程安全性不同:哈希表的方法是同步的,实现线程安全的地图,而HashMap中的方法不是同步的,是地图的非线程安全实现。
(4)。HashMap中和哈希表的地位不同:在并发环境下,哈希表虽然是线程安全的,但是我们一般不推荐使用它,因为有比它更高效,更好的选择的ConcurrentHashMap;而单线程环境下,HashMap中拥有比散列表更高的效率(哈希表的操作都是同步的,导致效率低下),所以更没必要选择它了。
2,哈希表与ConcurrentHashMap中的联系与区别
在“彻头彻尾的理解ConcurrentHashMap”一文中,我们知道ConcurrentHashMap引入了分段锁机制,因为这些差异是由它们的底层实现决定的。在“彻头彻尾的理解ConcurrentHashMap”一文中,我们知道ConcurrentHashMap引入了分段锁机制,在默认理想状态下,ConcurrentHashMap的可以支持16个线程执行并发写操作及任意数量线程的读操作;而哈希表无论在读的过程中还是写的过程中都会锁定整个地图,因此在并发效率上远不如ConcurrentHashMap的。
此外,哈希表和ConcurrentHashMap中对键值的限制相同,二者的关键和值都不允许是空。
七。更多
如果读者需要深入了解HashMap,请移步我的博文“Map综述(一):彻头彻尾理解HashMap”。
如果读者需要深入了解ConcurrentHashMap,请移步我的博文“彻头彻尾理解ConcurrentHashMap”。
更多关于哈希(哈希)和等于方法的介绍,请移步我的博文“Java中的==,等于与hashCode的区别与联系”。
更多关于Java SE进阶方面的内容,请关注我的专栏“Java SE进阶之路”。本专栏主要研究Java基础知识,Java源码和设计模式,从初级到高级不断总结,剖析各知识点的内在逻辑,贯穿,覆盖整个Java的知识面,在一步步完善,提高把自己的同时,把对Java的的所学所思分享给大家。万丈高楼平地起,基础决定你的上限,让我们携手一起勇攀的Java之巅......