对于Hashtable,其实实际中用的不多,但是作为一个面试常考点,还是试着来研究一下这个集合类。
首先研究Hashtable,就要将Hashtable和HashMap放在一起比较,因为两者的功能类似且相近。Hashtable产生于JDK1.1,而HashMap产生于JDK1.2,HashMap产生的时间要比Hashtable晚。接下来我们来看一看Hashtable的继承体系:
其实通过对比我们可以发现,Hashtable并不像HashMap之类的map集合类一样都属于AbstractCollection继承体系,它自成一个体系,继承自Dictonary抽象类,我们再来看看Dictonary类:
上图可以看出Dictonary和AbstractMap抽象类有些方法类似,只是AbstractMap抽象类的方法比Dictonary抽象类的方法更加全面,而且Dictionary类实际上已经是一个已经被废弃的类。接下来开始看其源码:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serialzable
可以发现Hashtable和HashMap除了继承的类不一样之外,其他实现的接口基本是一致的,此处就不再对这些接口的用途一一赘述了,毕竟都已经是老熟人了。下面继续看其成员变量:
private transient Entry<?,?>[] table;
//私有成员变量,不可序列化,哈希表
private transient int count;
//私有成员变量,不可序列化,键值对数量
private int threshold;
//私有成员变量,阈值
private float loadFactor;
//加载因子
private transient int modCount = 0;
//修改次数
private static final long serialVersionUID = 1421746759512286392L;
//序列化标志,用于反序列化
private transient volatile Set<K> keySet;
//私有成员变量,不可序列化,同步,用于获得key集合
private transient volatile Set<Map.Entry<K,V>> entrySet;
//私有成员变量,不可序列化,同步,用于获得Entry集合
private transient volatile Collection<V> values;
//私有成员变量,不可序列化,同步,用于获得value集合
从上可以发现其成员变量也和HashMap类似,都有哈希表table,count键值对数量,threshold阈值,loadFactor加载因子以及modCount修改次数,当然有些HashMap中存在的成员变量Hashtable中并没有出现,如最大容量,最大建树阈值之类的。让我们继续向下看:
public Hashtable(int initialCapacity, float loadFactor){
//构造器1,传入值为默认容量与加载因子
if(initalCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity:" + initalCapacity);
if(initalCapacity == 0)
initalCapacity = 1;
//判断initalCapacity是否合法,如果传入initalCapacity为0,则设置其为1
this.loadFactor = loadFactor;
table = new Entry<?,?>[initalCapacity];
//创建一个initalCapacity大小的数组赋给table,此处和HashMap不一样
threshold = (int)Math.min(initalCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//设置阈值
}
public Hashtable(int initalCapacity){
//构造器2,传入值为默认容量,则设置加载因子为0.75,并调用构造器1
this(initalCapacity, 0.75f);
}
public Hashtable(){
//构造器3,空参,则设置默认容量为11,设置加载因子为0.75,此处初始默认容量和HashMap不一样
this(11,0.75f);
}
public Hashtable(Map<? extends K, ? extends V> t){
//构造器4,传入的值为map集合
this(Math.max(2 * t.size(),11),0.75f);
putAll(t);
}
Hashtable有四个构造器,当传入值为初始容量与加载因子时,其会先判断传进来的值是否合法,然后直接创建一个初始容量大小的table数组,这与HashMap不同,HashMap是无论你传多大的初始容量进去,它会根据大小来创建相应的2次幂大小的table数组。当传入值为初始容量时,则先设置默认加载因子为0.75,再调用构造器1进行创建;当传入空参时,它设置初始容量为11,默认加载因子为0.75.这又和HashMap不一样,HashMap的默认初始容量是16。当传入的参数为map集合时,先取该集合的大小的两倍与默认初始容量11的最大值作为初始容量,并和默认加载因子0.75一起调用构造器1进行构造,再将map集合中的键值对依次放入Hashtable中。
那么为什么有些地方和HashMap不一样呢?我们来研究一下它的核心方法:扩容
@SuppressWarnings("unchecked")
protected void rehash(){
//哈希冲突的解决方式
int oldCapacity = table.length;
//获得当前的哈希表长度作为旧容量
Entry<?,?>[] oldMap = table;
//获取当前的table作为旧数组
int newCapacity = (oldCapacity << 1) + 1;
//定义新容量为旧容量的两倍加一
if(newCapacity - MAX_ARRAY_SIZE > 0){
//如果新容量比数组大小最大值还要大
if(oldCapacity == MAX_ARRAY_SIZE)
//如果旧容量就是最大值
return;
//不进行扩容,直接返回
newCapacity = MAX_ARRAY_SIZE;
//否则就将新容量定义为最大值
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//创建一个新的数组,数组的大小为新容量
modCount++;
//修改次数加一
threshold = (int)Math.min(newCapacity * loadFactor,MAX_ARRAY_SIZE + 1);
//设置阈值
table = newMap;
//将newMap赋给table
for(int i = oldCapacity; i-- > 0; ){
//遍历数组槽
for(Entry<K,V> old = (Entry<K,V>)oldMap[i]; old != null; ){
//遍历每一个数组槽内的链表
Entry<K,V> e = old;
//先取出当前结点作为e
old = old.next;
//再将指针向后移动一位
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//重新定位
e.next = (Entry<K,V>)newMap[index];
//将开始存在与newMap[index]处的结点作为e的下一个结点
newMap[index] = e;
//再将e放入newMap[index]中,即如果在newMap[index]处的是null
//则将e直接放入,且下一个结点为null,如果不是,则将e插入到链表的头部
}
}
}
我们可以发现,Hashtable中的rehash方法与HashMap中的最大的不同除了不需要考虑红黑树之外,就是定位方法不同了,它的hash定位方法是:
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
首先 0x7FFFFFFF以二进制位数表示为:
0111 1111 1111 1111 1111 1111 1111 1111
而位与运算的计算方法是1 & 1为1,其实为0.因此将结点的hash值与0x7FFFFFFF进行与运算的目的就是将最高位全部转换为0,也就是无论正数负数都转换为正数。再和newCapacity进行求余运算我们就很容易理解了,因为我们在介绍HashMap中的hash定位时使用的第一个散列函数就是取余运算。因为是采用取余运算来通过hash值来计算在散列表中的位置,因此对于定位计算时二进制每一位是多少就没有那么苛刻的要求,所以你可以选择任何大小(合法的)作为table的大小都没有问题。而默认初始容量也不必设置成16,而由于Hashtable产生于HashMap之前,它当时采用的是以11位table的默认初始大小的。但是每一次扩容时都是原容量乘以2再加1,也就是尽量时散列表大小为奇数,这样可以尽量将插入的元素在散列表中均匀分布,减少哈希冲突。
那么Hashtable是如何处理哈希冲突的呢?
1.put
public synchronized V put(K key, V value){
//增加键值对,对外方法
if(value == null){
//首先判断value是否为空,Hashtable中不支持value值为空
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//获取table
int hash = key.hashCode();
//获取key的哈希值
int index = (hash & 0x7FFFFFFF) % tab.length;
//获取应该插入的位置
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//获取该位置当前的键值对
for(; entry != null; entry = entry.next){
//如果该位置上有结点,则遍历结点,寻找是否有和传入进来key一样
//的key,如果存在,则直接用传进来的value将该key对应的value覆盖
//并且返回原来的key,因此从此处可以看出,Hashtable不允许key重复
//如果插入的键值对有重复,则将Hashtable中的value进行覆盖
if((entry.hash == hash) && entry.key.equals(key)){
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash,key,value,index);
//否则作为链表的头结点放入数组槽内
return null;
//返回null代表插入成功
}
从上述增加键值对的方法就可以看出其是如何处理hash冲突了,当通过hash值定位到散列表的某一个槽内后,发现槽内已经有一个键值对存在了,则将新插入的键值对的后置指针指向槽内的键值对,并且将新键值对放入槽内,也就是拉链法。但是只止步于拉链法,并没有像HashMap中那样再建红黑树。除此之外,我们还可以发现Hashtable中不允许存在相同的key值并且不允许value值为空。
接下来继续看常用方法:
2.remove
public synchronized V remove(Object key){
//通过key值删除Hashtable中的某一个键值对
Entry<?,?> tab[] = table;
//获得table
int hash = key.hashCode();
//获得key的hash值
int index = (hash & 0x7FFFFFFF) & tab.length;
//获得位置
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null; e != null; prev = e, e = e.next){
//遍历槽内的链表
if((e.hash == hash) && e.key.equals(key)){
//如果寻找到了
modCount++;//修改次数加一
if(prev != null){
//如果需要删除的元素不是首结点
prev.next = e.next;
//将前一个结点的后置指针指向当前结点的后置结点,即完成删除
}else{
tab[index] = e.next;
//否则,说明是首结点,则将下一个结点作为首结点即可
}
count--;
//数量减一
V oldValue = e.value;
e.value = null;//置空,方便GC回收
return oldValue;//返回被删除结点的value
}
}
return null;//如果返回null,则说明删除失败
}
同样,remove方法也和HashMap中的方法类似,当删除成功时则返回删除元素的value值,否则返回Null