概述
Hashtable,很大程度上和HasMap的实现差不多,但也有不同,之后我们会阐述。Hashtable和HashMap都可以将key和value结合起来构成键值对,通过put(key,value)方法保存起来,然后通过get(key)方法获取相应的value。
Hashtable类定义
Hastable在Java中定义:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
/...
}
我们发现,去HashMap不同的是,Hashtable继承于Dictionary类,这个类是将任何键映射到相应值的类的抽象弗雷。每个键和每个值都是一个对象。在任何一个Dictionary对象中,每个键至多与一个值相关联。
Hashtable中的属性
我们来看看Hashtable的类属性:
// Entry[]数组:用于存放集合中的元素
private transient Entry<?,?>[] table;
// 集合中Entry键值对的数量。注意count不是容器的大小。
private transient int count;
// 阈值
private int threshold;
// 加载因子
private float loadFactor;
//记录集合修改的次数。
private transient int modCount = 0;
private static final long serialVersionUID = 1421746759512286392L;
类构造器:
// 默认构造器,默认创建一个容量为11的Entry[]数组,且加载因子为0.75
public Hashtable() {
this(11, 0.75f);
}
// 带有一个参数的构造器,其中调用的是带有两个参数的构造器。
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable(int initialCapacity, float loadFactor) {
// 判断参数合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 判断参数合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
// initalCapacity为0时,用 1 赋值
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
// 创建容器大小。
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 构造一个与给定的 Map 具有相同映射关系的新哈希表。
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
主要方法
put()方法:将指定key映射到此哈希表中的指定value。Hashtable中的key和value不能为空。
public synchronized V put(K key, V value) {
// 确保value不为空
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode(); //计算哈希值
int index = (hash & 0x7FFFFFFF) % tab.length; //计算该key在table中的索引位置
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];//拿到这个索引下的结点
// for循环主要是遍历该位置下的所有结点是否有和该key相同的
for(; entry != null ; entry = entry.next) {
// 如果有,则新值覆盖旧值
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 如果没有,则在该位置上,通过拉链法添加该key-value键值对。
addEntry(hash, key, value, index);
return null;
}
Hashtable在put()方法中,调用了addEntry()方法添加一个新的键值对,我们来看一看这个方法:
private void addEntry(int hash, K key, V value, int index) {
// 对集合的修改次数+1
modCount++;
Entry<?,?> tab[] = table;
// 如果键值对个数超过了阈值,则进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
// 扩容操作
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 添加一个新的键值对。 @SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
我们再来看一看扩容:rehash():
@SuppressWarnings("unchecked")
protected void rehash() {
// 获取原数组长度
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
// 扩容至原数组的2倍+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;
}
// new一个新的数组。
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 计算阈值。
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 计算原数组中每个index位置上的结点在新数组中的位置。
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;
// 这里使用e.hsah && 0x7FFFFFFF目的是为了尽可能保持与原数组的顺序相同。
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
get():
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;
}
通过计算出key的hash值进而计算出在数组中的index位置,然后遍历这个index上的所有结点,找到了返回该key对应的value值,没有找到返回null。
总结
Hashtable中存储键值对采用的是 “链地址法” ,且Hashtable是线程安全的。我们发现,Hashtable中的方法都被synchronized修饰,都被加了一把锁,所以,在多线程环境下,Hashtable是线程安全的。
Hashtable和HashMap的区别:
① Hashtable继承Dictionary类,而HashMap继承AbstractMap类。Dictionary类是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于Map接口的实现,以最大限度地减少实现Map接口所需要的工作。
② HashMap可以存储键为null,值为null的键值对,但是HashTable的key和value都不允许为null。当Hashtable的value为null时,为抛出异常。
if (value == null) {
throw new NullPointerException();
}
③ HashMap是线程不安全的,Hashtable是线程安全的, Hashtable中的方法使用synchronized修饰,在多线程环境下线程安全,但效率不高。Collections中提供了一个synchronizedMap()方法,可以创建一个线程安全的Map对象,所以如果我们要使用HashMap并要保证线程安全的话,我们可以调用Collections中的synchronizedMap()方法。