HashTable整体架构
HashTable实现哈希表,该哈希表将键映射到值。可以是任何不为空的key或者value。主要是因为HashTable的存储或者检索key必须实现hashCode 和equals方法,其类图如下:
类注解信息
- 此类实现哈希表,该哈希表将键映射到值。任何非null 对象可以用作键或值。
- 为了成功地从哈希表存储和检索对象,用作键的对象必须实现hashCode方法和 equals 方法。
- Hashtable 的实例具有两个影响其参数的参数性能:初始容量和负载系数。HashTable的容量是哈希表中存储桶的数量,初始容量只是哈希表时的容量 被建造。请注意,哈希表是open:对于“哈希”冲突”,单个存储桶存储多个条目,必须对其进行搜索按顺序.加载因子是散列填充量的度量允许在自动增加其容量之前获取表。初始容量和负载因子参数仅提示实施。有关何时以及是否进行重新哈希处理的确切详细信息调用的方法取决于实现。
- 通常,默认负载因子(.75)在时间和空间成本。较高的值会减少空间开销,但增加查找条目的时间成本(这在大多数情况下都得到了体现Hashtable操作,包括 get 和 put。
- 初始容量控制着浪费空间与需要进行 rehash操作,这非常耗时。如果初始,则不会进行rehash 操作容量大于最大条目数 Hashtable 将包含除以其负载因子。然而,将初始容量设置得太高会浪费空间。
- 如果要在 Hashtable 中创建许多条目,创建足够大的容量可能会允许比使其执行更有效地插入条目根据需要自动重新哈希以增长表。
重要的成员变量
/**
* 存放哈希表的数据
*/
private transient Entry<?,?>[] table;
/**
* hashtable中实际数据个数.
*/
private transient int count;
/**
*当表的大小超过此阈值时,表将被重新映射。 (
* 此字段的值为(int)(容量* loadFactor)
*/
private int threshold;
/**
* hashtable承载因子.
*/
private float loadFactor;
/**
* 记录hashtable版本修改
*/
private transient int modCount = 0;
初始化方式
- 无参初始化
public Hashtable() {
// 实际传入了初始值为initialCapacity为11,loadFactor为0.76f
this(11, 0.75f);
}
- 传入指定的initialCapacity的初始化方法。
public Hashtable(int initialCapacity) {
// initialCapacity为传入的参数,而loadFactor为0.75f
this(initialCapacity, 0.75f);
}
- 传入指定的initialCapacity和loadFactor方式的初始化。
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);
// 其次
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
// 阈值是通过initialCapacity×loadFactor 与MAX_ARRAY_SIZE + 1)做比较,那个小一点则使用那个作为阈值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
- 传入初始值数据的初始化方式。
public Hashtable(Map<? extends K, ? extends V> t) {
// initialCapacity的值是根据传入的map的大小×2与默认大小11做比较,
// 两者中取最大值作为initialCapacity的值,loadFactor还是使用默认的0.75f
this(Math.max(2*t.size(), 11), 0.75f);
// 调用批量添加的方式进行添加。
putAll(t);
}
新增方法(put)
HashTable新增数据的大致流程
- 对入参的key 和value做数据校验,key和value都不能为空.
- 计算新的数据节点的key对应到数组的响应的数组索引。如果该索引的位置已经有值,则进行覆盖,不存在则调用新增addEntry()方法新增数据节点。
- 检查数组中的大小大于阈值的进行扩容,走4。否则直接创建新的 Entry<K,V> e并存入数组table中,
- 计算出行的数组容量,新的数组容量为oldCapacity << 1 相当于 乘以2 在加1.
- 根据新计算的扩容大小进行扩容。
public synchronized V put(K key, V value) {
// 对传过来的参数进行数据的校验,如果value为null则抛出空指针.
if (value == null) {
throw new NullPointerException();
}
// 确保key尚未在哈希表中。
Entry<?,?> tab[] = table;
// 计算key的哈希值。如果为空则抛出空指针
int hash = key.hashCode();
// 计算出添加数据节点落在table的索引位置。
//0x7FFFFFFF为 最大的整型数 int就是除了首位是 0,其余都是1
// & 运算如果相对应位都是1,则结果为1,否则为0
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 遍历hashtable,如果key已经在hashtable中,则将新添加的数据value覆盖老的value值
// 并将老值返回
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 不存在则直接调用新增节点方法,并返回null
addEntry(hash, key, value, index);
return null;
}
addEntry添加节点
private void addEntry(int hash, K key, V value, int index) {
// 用于记录HashTable版本修改记录。
modCount++;
// 获取哈希桶中的数据
Entry<?,?> tab[] = table;
// 如果桶中的数量大于阈值,则进行扩容
if (count >= threshold) {
// 如果超过阈值,请重新哈希表
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方法
/**
* HashTable扩容
*/
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 有溢出意识的代码, <<1 num乘以2 再加一
int newCapacity = (oldCapacity << 1) + 1;
// 如果新计算的HashTable的容量大于Integer.MAX_VALUE - 8走if的逻辑
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 如果原先数组大小等于Integer.MAX_VALUE - 8
if (oldCapacity == MAX_ARRAY_SIZE)
// 继续使用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;
// 将原先的数据重新计算落在新的数组的位置
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;
}
}
}
批量添加putAll()
public synchronized void putAll(Map<? extends K, ? extends V> t) {
// 遍历传入的参数,然后一个添加。
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}
get方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 求出key对应的数组索引
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// 哈希值相等且key相等则返回e对应的value值
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
// 找不到则返回null
return null;
}
remove方法
/**
* 从中删除键(及其相应的值)哈希表。如果键不在哈希表中,则此方法不执行任何操作
* @param key 需要取下的键
*/
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
// 计算出key对应的数据位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
// 如果为空则不进行任何操作,则直接返回null
// 如果数组不为空,则遍历这个链表,找到哈希值和equals相等的值
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 {
// 如果为空,则进行将pre.next放在原来位置中
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
clear方法
// 清除哈希表中的值,
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;
}
contains方法
public synchronized boolean contains(Object value) {
// 不能为空。
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
// 遍历数组中的Entry的value相等则返回true,没有则为false
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
HashTable与HashMap区别
- 继承的父类不同。Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
- 线程安全性不同。Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。
- 是否提供contains方法。HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
- key和value是否允许null值。其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。
Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应 的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。 - 两个遍历方式的内部实现上不同。Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
- hash值不同。哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
- 内部实现使用的数组初始化和扩容方式不同。Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。