基于JDK1.8对Java中的Hashtable集合的源码进行了深度解析,包括各种方法、扩容机制、哈希算法、遍历方法等方法的底层实现,最后给出了Hashtable和HashMap的详细对比以及使用建议。
1 Hashtable的概述
public class Hashtable< K,V > extends Dictionary< K,V > implements Map< K,V >, Cloneable, Serializable
Hashtable是来自于JDK1.0时代的古老key-value形式的集合类。类当中所有的方法都是同步的,数据安全的,效率低。
JDK1.0的时候Hashtable是继承的抽象类Dictionary,JDK1.2集合框架诞生之后,又实现了Map 接口,成为了Java集合体系的一员。
实现了Cloneable、Serializable标志性接口,支持克隆、序列化操作。
由于Map不属于Collection集合体系,没有实现Iterable接口,因此不支持获取迭代器的方法iterator(),或者说Map的集合体系并没有真正的迭代器。但是它们有自己的遍历数据的方法。
Hashtable的底层实际上是采用“拉链法”实现了一个哈希表,即使用一个数组作为哈希表的骨架,每一个数组元素的位置称为“bucket”桶,桶里存放的就是哈希值相同的键值对,如果一个桶里面有多个键值对,那么说明出现了哈希冲突,Hashtable使用“拉链法”解决冲突,每个桶的大小即该位置链表节点数量。Hashtable的key 和 value 都不允许为null。
2 Hashtable的源码解析
2.1 主要类属性
/**
* 内部Entry[]数组,用来作为哈希表的骨架,数组每一个Entry元素代表了一个链表的头节点,Hashtable内部的哈希表的key-value键值对都是存储在Entry节点中的。
*/
private transient Entry<?, ?>[] table;
/**
* HashTable的大小,注意这个大小并不是HashTable的容器大小,而是他所包含Entry键值对的数量。
*/
private transient int count;
/**
* Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。当count大于等于threshold时,需要调整容量(尝试扩容)。
*/
private int threshold;
/**
* 加载因子,是可以大于1的。
*/
private float loadFactor;
/**
* 用来实现"fail-fast"机制的(也就是快速失败)。
*/
private transient int modCount = 0;
扩容阈值是由出初始容量和加载因子共同决定的,通常threshold=table.length*loadFactor,初始容量和加载因子越大,那么就不需要频繁的“扩容”,初始容量过大可能会浪费更多空间,加载因子越大会增加哈希冲突的风险,导致查找数据的时间过长。默认容量(11)和加载因子(0.75)在时间和空间成本上寻求一种折衷。
关于modCount 的作用和fail-fast机制,早在ArrayLsit集合的源码文章中就已经讲解了,java.util包下的集合的fail-fast机制都是一样的,这里不再赘述,详情可以看这篇文章:Java集合—ArrayList的源码深度解析以及应用介绍。
2.2 Entry节点
Entry实际上就是Hashtable的一个内部类,作为内部存储key和value的容器,还保存key的hashCode值,同时由于Hashtable采用“拉链法”实现哈希表,每一个Entry还作为链表的一个节点,因此内部还有一个到下一个节点的引用属性。
实际上Entry实现了Map.Entry接口,因此Entry内部还实现了相关方法共外部调用。EntrySet()方法返回的set集合的元素May.Entry,实际上就是返回的这个Entry节点的实例,后面会详细讲解!
private static class Entry<K,V> implements Map.Entry<K,V> {
//哈希值,存储起来方便后续使用,避免重复运算
final int hash;
//key
final K key;
//value
V value;
//下一个相同桶位的节点引用
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
据此,我们能够画出Hashtable的大概数据结构图:
2.3 构造器与初始化参数
2.3.1 Hashtable()
public Hashtable()
构造一个新的,空的散列表,默认初始容量(11)和加载因子(0.75)。
public Hashtable() {
//内部调用另外一个构造器,初始容量11,加载因子0.75
this(11, 0.75f);
}
2.3.2 Hashtable(int initialCapacity)
public Hashtable(int initialCapacity)
用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。
public Hashtable(int initialCapacity) {
//内部调用另外一个构造器,用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。
this(initialCapacity, 0.75f);
}
2.3.3 Hashtable(int initialCapacity, float loadFactor)
public Hashtable(int initialCapacity,float loadFactor)
用指定初始容量和指定加载因子构造一个新的空哈希表。加载因子可以大于1,但是很明显,加载因子越大,发生哈希冲突的概率也越大!
这里的initialCapacity也没有要求是2的幂次方,但是HashMap 中初始化容量大小必须是 2 的幂次方。
/**
* 建议数组最大容量,因为某些VM实现可能需要部分长度用来存放数组头部信息
* 但是在HotSopt的虚拟机中,数组长度是可以超过这个限制的,可以达到Integer.MAX_VALUE – 2的长度
* 并且在上面的源码中能够看到,我们分配的initialCapacity完全可以大于MAX_ARRAY_SIZE
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
//如果初始容量为0,则变成1
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);
}
2.3.4 Hashtable(Map<? extends K,? extends V> t)
public Hashtable(Map<? extends K,? extends V> t)
构造一个与给定的 Map 具有相同映射关系的新哈希表。该哈希表是用足以容纳给定 Map 中映射关系的初始容量和默认的加载因子(0.75)创建的。
public Hashtable(Map<? extends K, ? extends V> t) {
//首先初始化hashtable
this(Math.max(2*t.size(), 11), 0.75f);
//底层调用putAll方法
putAll(t);
}