文章目录
什么是 Hashtable ?
Hashtable 是一个散列表,它是以键值对来存储的,这点与 HashMap 一样
Hashtable 的继承结构
Hashtable 继承了 Dictionary,并且实现了 Map、Cloneable、Serializable 接口
其中 Dictionary 是一个抽象类,是任何可将映射到相应值的类的抽象父类,每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键最多只能与一个值相关联
Hashtable 几个重要的成员变量
在 Hashtable 中是通过 “拉链法” 来实现的。它包括几个重要的成员变量,如下:
// 单向链表,Hashtable 中的 key-value 键值对都是存储在这个 Entry 数组中的
private transient Entry<?,?>[] table;
// Hashtable 中存储的键值对的数量
private transient int count;
// Hashtable 的阈值,用于判断是否需要调整 Hashtable 的容量
private int threshold;
// 加载因子
private float loadFactor;
// 修改值,用来实现 fail-fast 机制用的
private transient int modCount = 0;
Hashtable 的构造函数
Hashtable(int initialCapacity, float loadFactor)
传入 initialCapacity(初始容量)和 loadFactor(加载因子)构建 Hashtable
// 设置初始大小和加载因子
public Hashtable(int initialCapacity, float loadFactor) {
// 如果初始化的容量 < 0 的话抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 如果加载因子小于 0 或者加载因子不是 float 类型的话抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
// 如果初始化容量为 0,则设置初始化容量为 1
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
// 初始化 table,设置 initialCapacity 大小的 table 数组
table = new Entry<?,?>[initialCapacity];
// 计算阈值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
Hashtable(int initialCapacity)
传入指定初始容量构建一个新的 Hashtable
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
Hashtable()
默认的构造函数,初始容量大小为 11,加载因子为 0.75 f
public Hashtable() {
this(11, 0.75f);
}
Hashtable(Map<? extends K, ? extends V> t)
构建一个与给定 Map 具有相同映射关系的新的 Hashtable
public Hashtable(Map<? extends K, ? extends V> t) {
// 设置 table 容器大小
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
Hashtable 的主要方法
put(K key, V value)
public synchronized V put(K key, V value) {
// Make sure the value is not null
// 确保 value 不能为空,否则会抛出异常
if (value == null) {
throw new NullPointerException();
}
// 确保 key 在 table 中是不可重复的,唯一的
Entry<?,?> tab[] = table;
// 计算 key 的 hash 值
int hash = key.hashCode();
// 将 key 的 hash 值进行与远算再除以数组的长度得到索引位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 迭代遍历,寻找该 key,进行替换
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;
}
addEntry(int hash, K key, V value, int index)
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
// 如果容器中的元素数量已经达到阈值,则进行扩容
if (count >= threshold) {
// 扩容方法
rehash();
// 移动到新的 Entry 数组
tab = table;
// 重新计算 hashCode
hash = key.hashCode();
// 重新计算索引位置
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 创建新的 Entry 数组
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
// 在索引位置插入新的节点
tab[index] = new Entry<>(hash, key, value, e);
// 容器中的元素 + 1
count++;
}
rehash()
扩容方法
protected void rehash() {
// 获取数组的长度
int oldCapacity = table.length;
// 获取数组的元素
Entry<?,?>[] oldMap = table;
// 新的容量 = 旧的容量 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
// 如果新的容量 - Integer.MAX_VALUE - 8 大于 0,也就是溢出了
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 如果旧的容量 == Integer.MAX_VALUE - 8
if (oldCapacity == MAX_ARRAY_SIZE)
return;
// 继续使用 Integer.MAX_VALUE - 8 作为新的容量
newCapacity = MAX_ARRAY_SIZE;
}
// 新建一个容量大小为 Integer.MAX_VALUE - 8 的 Entry 数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// 将新建的数组移动到 table
table = newMap;
// 将原来的元素拷贝到新的 Hashtable 中
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;
}
}
}
Hashtable 是如何确定存储位置的?
那么 Hashtable 是如何确定存储位置的呢,也就是索引的位置。在上述代码中也重复出现了计算索引的代码,我们将这段代码复制修改如下:
/**
* 计算索引位置
* @param hashCode 传入 key 的hashCode 值
* @return 返回索引位置
*/
public static int getIndex(int hashCode) {
int index = (hashCode & 0x7FFFFFFF) % 11;
return index;
}
OK,就是这么一段核心的代码,我们测试一下:
import java.util.Hashtable;
/**
* @author Woo_home
* @create by 2020/7/14 12:29
*/
public class Demo {
/**
* 计算索引位置
* @param hashCode 传入 key 的hashCode 值
* @return 返回索引位置
*/
public static int getIndex(int hashCode) {
int index = (hashCode & 0x7FFFFFFF) % 11;
return index;
}
public static void main(String[] args) {
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("name", "John");
hashtable.put("age", "18");
System.out.println(hashtable);
// 计算存储的索引位置
System.out.println("nameIndex : " + getIndex("name".hashCode()));
System.out.println("ageIndex : " + getIndex("age".hashCode()));
}
}
输出:
从输出结果可以发现,name 和 age 存储的索引位置分别是 7 和 8
get(Object key)
Hashtable 的 get() 方法,这个方法还是比较简单的
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 首先还是先计算 key 的hashCode 值
int hash = key.hashCode();
// 然后获得 table 数组中的索引位置
int index = (hash & 0x7FFFFFFF) % tab.length;
// 然后迭代链表
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
// 如果找到相对应的 key 的 value,则返回
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
// 如果没有找到则返回 null
return null;
}
Hashtable 与 HashMap 有什么区别?
1、是否线程安全
HashMap 是线程不安全的,Hashtable 是线程安全的,在 Hashtable 中内部的方法基本都是经过 synchronized 修饰的
2、效率
因为 Hashtable 是线程安全的,所以效率上比 HashMap 要低。另外,HashTable 基本要被淘汰了
3、键值能否为空?
在 HashMap 中,null 可以作为键,也可以作为值,但是只能有一个键为 null,可以有多个值为 null。而 HashTable 是不允许键值为 null 的,不然会抛出空指针异常
4、默认初始容量大小:
HashTable 在创建时如果不指定容量的初始值时,默认初始容量大小为 11。HashMap 默认的初始容量大小为 16
5、每次扩容大小
HashTable 每次扩容,容量会变成原来的 2n+1 倍,而 HashMap 每次扩容,容量会变成原来的 2 倍。如果创建时给定了容量的初始值,那么 HashTable 会直接使用给定的值,而 HashMap 会将其扩充为 2 的幂次方大小
6、底层数据结构
在 JDK 1.8 中的 HashMap 使用的是数组 + 链表 + 红黑树的数据结构,在解决哈希冲突时有了较大的变化,当链表长度大于阈值的的时候(默认为 8 时),将链表转换为红黑树,以减少搜索的时间。HashTable 没有这样的机制