前言:HashTable的底层也是基于散列表来实现的,和HashMap有着千丝万缕的联系,作为map家族另外一个常用集合面试中也是经常被问到这两者之间的区别,对其源码的解读肯定对我们有很大的提升。本文将会围绕其数据结构、构造方法、以及里面的常用方法进行展开,来解析HashTable的源码。以最简单的方式,来读懂最难的代码。
一:HashTable的具体实现
HashTable的数据结构和HashMap的数据结构大致一致,都是利用数组和链表来实现的(jdk1.8HashMap加入了红黑树),两者最大区别在于HashTable是线程安全的,但是HashMap不是线程安全的,具体的图示可以参考基于jdk1.8的HashMap的源码分析
1.1:HashTable的成员变量
//保存数据的数组
private transient Entry<?,?>[] table;
//HashTable所包含Entry键值对的数量
private transient int count;
//扩容的阀值
private int threshold;
//加载因子
private float loadFactor;
//内部元素修改的次数,为了防止出现并发修改异常
private transient int modCount = 0;
1.2:HashTable的链表节点Entry<K,V>
//基本和HashMap的链表节点是一致的
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
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()));
}
// Map.Entry Ops
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();
}
}
1.3:HashTable的构造方法
//方法一:默认的容量为11、加载因子为0.75 那么扩容阀值就是 11*0.75 = 8
public Hashtable() {
this(11, 0.75f);
}
//方法二:给出一个初始容量
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
//方法三:给出初始容量和加载因子
public Hashtable(int initialCapacity, float loadFactor) {
//如果初始容量小于0、抛错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//加载因子小等于0或者是NAN值、抛错
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//根据初始化的容量来创建数组
table = new Entry<?,?>[initialCapacity];
//计算阀值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
1.3:HashTable的常用方法
1.3.1:HashTable的put()方法
//注意的是加了synchronized关键字
public synchronized V put(K key, V value) {
// Make sure the value is not null
//值不能为null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
//接下来的操作是确保key在table中不是重复的
Entry<?,?> tab[] = table;
//获取key的哈希值
int hash = key.hashCode();
//用key的hash和0x7FFFFFFF=Integer.MAX_VALUE=2的32次方-1进行 & 运算后 取模获取下标
//其实就是确认key的索引位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//对当前链表进行遍历
for(; entry != null ; entry = entry.next) {
//如果hash想等且key也相等
if ((entry.hash == hash) && entry.key.equals(key)) {
//进行替换,返回旧值
V old = entry.value;
entry.value = value;
return old;
}
}
//如果添加元素
addEntry(hash, key, value, index);
//成功后返回 null
return null;
}
private void addEntry(int hash, K key, V value, int index) {
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;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//创建一个新节点并且插入在链表的头节点
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
//扩容机制
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;
}
//扩容后的数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//重新计算扩容阀值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
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的扩容机制如下:例如默认初始容量是11,加载因子为0.75,那么扩容阀值就是8,当数组长度达到8的时候,HashTable就会进行一第次扩容,扩容后的容量就是 8 * 2 + 1 = 17 ( int newCapacity = (oldCapacity << 1) + 1) ,此时的扩容阀值就是 17 * 0.75 = 13 ,当下次达到13的时候,就会在重复扩容一次。其实,这个扩容消耗还是蛮大的,因为扩容后需要原来HashTable中的元素一一复制到新的HashTable中。
1.3.2:HashTable的get()方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//计算hash
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;
}
}
//没有返回 null
return null;
}
1.3.3:HashTable的clear() 方法
//将元素全部置为null
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;
}
1.3.4:HashTable的containsKey() 方法
public synchronized boolean containsKey(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 true;
}
}
return false;
}
1.3.5:HashTable的containsValue() 方法
public boolean containsValue(Object value) {
return contains(value);
}
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
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;
}
二:HashMap和HashTable之间的区别
不同点:
- HashMap中key和value均可以为null,但是HashTable中key和value均不能为null。
- HashMap中默认容量的大小是16,而HashTable中默认数组容量是11。
- HashMap计算索引的方式是h&(length-1),而Hashtable用的是模运算,效率上是低于HashMap。
- HashMap1.8以后是采用数组+链表+红黑树结构且元素在尾部添加,HashTable采用的是数组+链表的结构,元素在头部添加。
- HashMap中数组容量的大小是2的n次方,如果初始化时不符合要求会自己进行调整,而HashtTable中数组容量的大小可以为任意正整数。
- HashMap不是线程安全的,而hashtable是线程安全的,HashTable中的get和put方法均采用了synchronized关键字进行了方法同步。
相同点
- HashMap和Hashtable存储的是键值对对象,而不是单独的键或值。
- HashMap和Hashtable默认负载因子都为0.75。
- HashMap和Hashtable都是通过链表法解决冲突;
- HashMap和Hashtable都不允许键重复,若键重复,则新插入的值会覆盖旧值。