1.定义
Hashtable的定义为:首先它是类似与HashMap的key-value的哈希表,不允许key-value为NULL值,另外一点值得注意的是Hashtable是线程安全的,根据Hashtable的这里将提出几个问题:
1.Hashtable的内部实现是怎样的?
2.Hashtable是如何实现线程安全的?
3.同样是key-value的集合,Hashtable与HashMap的区别是什么?
下文将围绕以上三个问题进行,另外本文源码来自与JDK_1.8.0_131
2.结构
2.1 类图结构
如上图所示,为Hashtable的类图结构,其主要实现、继承的接口、类如下:
- 1.Dictionary 类: 该抽象类定义了key-value的集合,定义每个key与value都是一个对象,不允许null值作为key与value,值得注意的是这个类已经过时了,新的实现都需要实现Map接口而不是该抽象类
- 2.Map 接口: 定义将键值映射到值的对象,Map规定不能包含重复的键值,每个键最多可以映射一个值,这个接口是用来替换Dictionary类.
- 3.Cloneable 接口: 实现了该接口的类可以显示的调用Object.clone()方法,合法的对该类实例进行字段复制,如果没有实现Cloneable接口的实例上调用Obejct.clone()方法,会抛出CloneNotSupportException异常。正常情况下,实现了Cloneable接口的类会以公共方法重写Object.clone()
- 4.Serializable 接口: 实现了该接口标示了类可以被序列化和反序列化,具体的 查询序列化详解
2.2 基本属性及构造方法
2.2.1 基本属性
如下源码所示,Hashtable中基本的属性如下,值得注意的是首先Hashtable中实际上记录一个数组,即Entry<?,?>[] table,这便是Hashtable的内部结构,再就是加载因子loadFactor这个属性,这个属性和初始容量影响了Hashtable的性能,loadFactor的默认值为0.75,这个是从时间和空间的角度综合考虑后设定的,在没有特殊的情况下建议不用修改这个值。
//Hashtable内部数组结构
private transient Entry<?,?>[] table;
//Hashtable元素总个数
private transient int count;
//扩容操作的阈值
private int threshold;
//加载因子 一般情况下为0.75
private float loadFactor;
//记录当前修改信息
private transient int modCount = 0;
2.2.2 构造方法
Hashtable有4个构造方法,如下所示:
- public Hashtable(int initialCapacity, float loadFactor):参数指定初始化容量和加载因子
- public Hashtable(int initialCapacity):参数指定初始化容量,其内部实际上是调用了上一个构造方法,其加载因子是默认的值 0.75
- public Hashtable():默认的构造方法,其内部调用了第一个构造方法,指定的初始化容量为11,加载因子是 0.75
- public Hashtable(Map<? extends K, ? extends V> t):将一个Map类型的集合全部添加到Hashtable中,内部实际调用了putAll方法
3.原理
3.1 内部结构
上图为Hashtable的内部结构,实际上我们通过Entry<?,?>[] table就可以看出,Hashtable内部为一个Entry<?,?>类型的数组,而Entry的结构如下所示,从而可以看出Hashtable是数组连表,既然是一个数组链表就会存在hash冲突的情况,下面就通过Hashtable中的实现细节,来探寻其中的奥秘。
private static class Entry<K,V> implements Map.Entry<K,V> {
//hash值
final int hash;
//对应元素的key值
final K key;
//对应元素的value值
V value;
//指向下一个节点的引用
Entry<K,V> next;
//...省略部分代码
}
3.2 实现细节
3.2.1 put方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
如上为Hashtable中添加key-value的方法,通过源码可以看出主要流程分为以下几步:
- 1.判空处理:对于value为空的情况,将抛出NullPointerException,在定义时发现Hashtable是不允许key,value都为null的,但这里为什么没有加以判断呢,原来每个key值将会获取其hash,即必须调用key.hashCode()方法,此时key为null是也会抛出NullPointerException,这也就是为什么Hashtable不允许key,value为NULL值。
- 2.定位:这一步其实很好理解,由于Hashtable是数组链表结构,首先需要定位到其在数组中的位置,使用(hash & 0x7FFFFFFF) % tab.length的方式,有可能你会奇怪 hash & 0x7FFFFFFF 这个有什么作用,我的理解是因为hash值是int类型,那么hash值有可能是负数,而负数的二进制标志是最高位,则和0x7FFFFFFF做与操作即是将负数变成正数,确保了获取到的index是正数。
- 3.遍历:遍历主要是查看是否已经存在需要添加的key-value,若已经存在则用新值替换老值,并返回老值,否则新增节点,这个操作主要是在addEntry方法中进行,如下是addEntry方法的源码,其流程是判断当前元素个数是否大于扩容阈值,若大于则rehash,否则新增节点并将该节点添加到对应的位置
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判断当前元素个数是否大于扩容阈值,若大于则rehash,否则
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
3.2.2 rehash方法
在上文中提到在当前元素个数大于扩容阈值时,会调用rehash方法进行扩容操作并且重新分布元素的位置,而阈值threshold=capacity * loadFactor,所以当capacity一定时,可以通过负载因子loadFactor去控制阈值的大小,负载因子loadFactor越大则阈值threshold越大,反而
负载因子loadFactor越小则阈值threshold越小,可以根据实际情况调整负载因子的大小从而调节Hashtable的性能。
下面为rehash方法的源码
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//扩容为源码的2*oldCapacity + 1;
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
//新建扩容后的数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//重新分布元素到不同的位置
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
从源码中可以看出rehash的过程实际上是扩容并重新分布的过程,主要包括以下几个步骤:
- 1.扩容:需要注意的是扩容是2*原容量 + 1
- 2.创建新数组:创建一个新的Entry<?,?>[],其容量为扩容后的新的容量
- 3.分布元素:将旧数组中的元素分布到新数组中
3.2.3 get方法
如下为Hashtable通过key值获取对应的value值的方法,其流程比较简单,和添加中存在部分类似,根据key值定位(此时若key为null,也将会报NullPointerException),然后遍历查找对应的值,若没找到则返回null
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
3.2.3 常用方法
如下列表中是Hashtable中常用的方法及其说明
方法 | 说明 |
---|---|
public synchronized boolean contains(Object value) | 测试此映射表中是否存在与指定值关联的键。 |
public synchronized V get(Object key) | 返回指定键所映射到的值 |
public synchronized V put(K key, V value) | 将指定 key 映射到此哈希表中的指定 value。 |
public synchronized V remove(Object key) | 从哈希表中移除该键及其相应的值。 |
public synchronized void clear() | 将此哈希表清空,使其不包含任何键。 |
public boolean containsValue(Object value) | 判断Hashtable是否包含某个值 |
public synchronized boolean containsKey(Object key) | 判断Hashtable是否包含指定的key |
4.对比
在JAVA学习-HashMap详解一文中详细介绍了HashMap的实现细节,其实通过本文的介绍你会发现Hashtable与HashMap存在很多相似的地方,下面来介绍下HashMap与Hashtable的区别:
- 1.实现:HashMap继承的类是AbstractMap类,而Hashtable继承的是Dictionary类,而Dictionary是一个过时的类,因此通常情况下建议使用HashMap而不是使用Hashtable
- 2.内部结构:其实HashMap与Hashtable内部基本都是使用数组-链表的结构,但是HashMap引入了红黑树的实现,内部相对来说更加复杂而性能相对来说应该更好
- 3.NULL值控制:通过前面的介绍我们知道Hashtable是不允许key-value为null值的,Hashtable对于key-value为空的情况下将抛出NullPointerException,而HashMap则是允许key-value为null的,HashMap会将key=null方法index=0的位置。
- 4.线程安全:通过阅读源码可以发现Hashtable的方法中基本上都是有synchronized关键字修饰的,但是HashMap是线程不安全的,故对于单线程的情况下来说HashMap的性能更优于Hashtable,单线程场景下建议使用HashMap.
总的来说,建议在单线程的情况下尽量使用HashMap。
5.总结
本文主要介绍了Hashtable内部实现,通过源码了解其内部的流程,最后也也介绍了Hashtable与HashMap的区别,若有问题,望指正。