概述
Hashtable作为早期的集合,在非并发的情况下,Hashtable已经基本被HashMap取代。而在有同步需求的时候,也可以使用效率更高的ConcurrentHashMap,而不是每个方法都用synchronize修饰的Hashtable。但同时,Hashtbale相对于HashMap与ConcurrentHashMap的来说,实现非常简单。通过Hashtable。我们可以大致了解java中散列表实现的基本原理,为HashMap与ConcurrentHashMap的学习打下基础。
顾名思义,Hashtable是散列表的一种的实现。与List相比,Hashtable的特点是可以存储key-value键值对,并且通过key来快速访问键值对。是不是很像通过下标访问对应元素的数组?与数组相比,Hashtable的优势在于key可以为任意类型。如果数组的下标可以是任意对象,我们可以这样实现一个简易的Hashtable:
public class SimpleHashtable{
Object[] table=new Object[16];
void put(Object key,Object obj){
table[key]=obj;
}
Object get(Object key){
return table[key];
}
}
不过,java中数组的下标只能是int。那么,如果我们找到一种方法,可以让所有类型的key都映射到int呢?这种方法,通常被称为散列函数。最明显的散列函数恐怕就是hashcode了:
public class SimpleHashtable{
Object[] table=new Object[16];
void put(Object key,Object obj){
table[hash(key)]=obj;
}
Object get(Object key){
return table[hash(key)];
}
int hash(Object obj){
// 将hashcode映射成下标
int index=obj.hashcode()%table.length;
return index;
}
}
但是在SimpleHashtable中,如果数组下标重复了(a.hashcode()==b.hashcode()),新值将会覆盖旧值。这种情况叫做散列冲突,可以利用链表来解决。用链表解决的方法叫做称为拉链法。采取拉链法,除了可以处理散列冲突,还可以避免最大数组容量2^31-1所带来限制:
public class SimpleHashtable{
// 数组中每个元素都是一个list
// 用Entry来存储键值对
List<Entry>[] table=new List<Entry>[16];
// 添加元素的时候加入下标对应的list中
void put(Object key,Object obj){
table[hash(key)].add(new Entry(key,obj));
}
// 获取的时候,遍历list寻找相等Entry
Object get(Object key){
for(Entry entry:table[hash(key)]){
if(entry.key.equals(key)){
return entry;
}
}
}
int hash(Object obj){
// 将hashcode映射成下标
int index=obj.hashcode()%table.length;
return index;
}
// 存储键值对的结构
class Entry{
Object key;
Object value;
}
}
注意到,SimpleHashtable中的table容量只有16。当SimpleHashtable的元素远远超过16时,get方法的效率将会大大降低,所需时间逐渐逼近于遍历list,远远超过了table[i]。所以,类似于ArrayList,SimpleHashtable需要有一个方法扩容table,来降低散列冲突,减少遍历list的机会与时间。
具体的做法就是,设置一个门槛值threshold,一旦元素的数量大于threshold的时候,就进行table的扩容,并进行元素的重新散列。
public class SimpleHashtable{
List<Entry>[] table=new List<Entry>[16];
// 元素数量
int size;
// 元素数量大于threshold时,扩容table
int threshold;
void put(Object key,Object obj){
table[hash(key)].add(new Entry(key,obj));
size++;
// 元素数量大于threshold时,扩容table,并重新进行散列
if(size>threshold){
rehash();
}
}
Object get(Object key){
for(Entry entry:table[hash(key)]){
if(entry.key.equals(key)){
return entry;
}
}
}
int hash(Object obj){
int index=obj.hashcode()%table.length;
return index;
}
void rehash(){
// 构建新table
// 重新散列元素至新table
// 设置下一次扩容的threshold
}
class Entry{
Object key;
Object value;
}
}
至此,SimpleHashtable便大体完成了。有了SimpleHashtable作为铺垫,理解Hashtable便容易许多。
接下来,便让我们看看Hashtable的主要方法吧。
get方法与put方法
首先是get方法,很简单,就是先根据哈希值计算在table里面的下标i,然后遍历table[i]这个单向链表。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 根据hashcode获得对应下标
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// 遍历entry list寻找key值对应元素
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
接着是put方法,有点长,但也很容易理解。主要分为两个步骤,第一步先遍历table[i],如果在table[i]中已经存在key,就替换原值并返回原值;如果不存在,就在table[i]中新加结点,注意这里添加的结点是头插法,也就是table[i]指向的结点变成了新结点。
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;
// 根据hashcode获得对应下标
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 遍历entry list判断key是否已存在,若存在,则替换原值
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;
}
private void addEntry(int hash, K key, V value, int index) {
Entry<?,?> tab[] = table;
// 元素数量超过threshold,进行再散列
if (count >= threshold) {
// 进行再散列
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];
// 添加新的Entry结点:将新结点添加到e之前
tab[index] = new Entry<>(hash, key, value, e);
count++;
modCount++;
}
再散列方法:rehash
在addEntry方法中,我们看到Hashtable使用count与threshold字段,来判断是否需要进行再散列。而Hashtable也有SimpleHashtable中rehash()的几大步骤:构建新table、 重新散列元素至新table、设置下一次扩容的threshold。
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)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
/* 设置下一次扩容的threshold */
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// table指向新数组
table = newMap;
/* 重新散列元素至新table */
// 遍历原table
for (int i = oldCapacity ; i-- > 0 ;) {
// 遍历Entry链表,重新散列元素
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;
}
}
}
遍历
遍历散列表是非常常见的操作,和Map差不多的三种遍历方式(key、value、entry)无需多言,除此之外,Hashtable还可以通过Enumeration遍历key或者value:
public synchronized Enumeration<K> keys() {
return this.<K>getEnumeration(KEYS);
}
public synchronized Enumeration<V> elements() {
return this.<V>getEnumeration(VALUES);
}
Hashtable并没有提供通过Enumeration遍历entry的方法,不过有意思的是,Enumerator的实现中却可以返回entry:
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
public T nextElement() {
// ...
if (et != null) {
Entry<?,?> e = lastReturned = entry;
entry = e.next;
// 可以返回key/value/entry
return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
}
throw new NoSuchElementException("Hashtable Enumerator");
}
}
为什么已经实现,却没有相应的api呢?我们来看看Enumerator实现的Iterator方法:
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
// Iterator methods
public T next() {
if (Hashtable.this.modCount != expectedModCount)
throw new ConcurrentModificationException();
return nextElement();
}
}
原来Iterator的next方法也是使用nextElement()实现的,所以返回的entry是为了支持通过Iterator遍历entry。