注:本文基于Android的HashTable
一些概念性的东西,比如哈希表、冲突等,请先阅读HashMap解析(建议先看这边文章)
秉承我一贯的风格,带着问题去一步一步解析HashTable的实现原理:
1、HashTable的数据结构是什么?
2、HashTable默认容量是多少?
3、HashTable最大容量是多少?
4、HashTable默认扩容阈值是多少?加载因子默认值是多少?
5、HashTable每次扩容大小是多少?
6、HashTable hash函数是怎么实现的?
7、HashTable get的原理是什么?
8、HashTable put的原理是什么?
9、HashTable 是否是线程安全?
10、HashTable与HashMap的区别是什么?
下面我根据源码一一解答上面的问题:
问题1:HashTable的数据结构是什么?
起始它跟HashMap一样使用的是一位数组+链表的数据结构,不同的是HashMap的节点使用的是Node内部类,而HashTable使用的是HashTableEntry内部类。看如下源码:
/**
* The hash table data.
*/
private transient HashtableEntry,?>[] table;
private static class HashtableEntry implements Map.Entry {
// END Android-changed: Renamed Entry -> HashtableEntry.
final int hash;
final K key;
V value;
HashtableEntry next;
//...省略
}
问题2:HashTable默认容量是多少?
跟HashMap不一样的是,HashTable并没有定义一个默认容量的常量,我们可以理解为HashTable的默认容量为11,HashTable的初始容量大小根据不同的构造函数定义的,我们来看下其构造函数:
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;
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(Map extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
通过上面代码,默认大小确实是11,只是它的初始容量可能不一样:
- 如果initialCapacity 未0,则初始容量为1
- 如果new HasTable不指定容量大小,默认是11,初始容量也是11
- 如果new时,传入的Map容量比较默认大小11大,那么初始容量大小为传入Map大小的2倍;否则默认默认的11
问题3:HashTable最大容量是多少?
Hashtable最大容量是Integer.MAX_VALUE - 8,为什么是Integer.MAX_VALUE - 8而不是Integer.MAX_VALUE,因为部分虚拟机占用了前8位,如果超出了Integer.MAX_VALUE - 8虚拟机会抛出OutOfMemoryError
我们来看下源码中的定义:
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
源码明确的定义了最大容量的值,且注释说明了为什么是Integer.MAX_VALUE - 8。
HashMap中即使页指定了最大容量的值为2^30,但是却允许在扩容操作时扩容到Integer.MAX_VALUE ,那么HashTable会不会跟HashMap一样处理呢?我们来看HashTable扩容操作的源码:
protected void rehash() {
//当前散列表的容量
int oldCapacity = table.length;
//当前散列表
HashtableEntry,?>[] 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
/**如果扩容的容量比限定的容量大,并且原容容量等于MAX_ARRAY_SIZE,则返回不做扩容处理
*/
return;
/** 如果扩容的容量比限定的容量大,并且原容容量不等于MAX_ARRAY_SIZE,则修正扩融入为限定的最大值
*/
newCapacity = MAX_ARRAY_SIZE;
}
//创建一个新的HashtableEntry一维数组,其容量为扩容之后的大小newCapacity
HashtableEntry,?>[] newMap = new HashtableEntry,?>[newCapacity];
//HashTable修改此时加1
modCount++;
//设置扩容阈值为newCapacity * loadFactor, MAX_ARRAY_SIZE + 1中的最小值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//使用新的扩容大小从新计算数组的下标值,并设定新的数组对应的值
for (int i = oldCapacity ; i-- > 0 ;) {
for (HashtableEntry old = (HashtableEntry)oldMap[i] ; old != null ; ) {
HashtableEntry e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (HashtableEntry)newMap[index];
newMap[index] = e;
}
}
}
上述代码证明了HashTable与HashMap最大容量大小在扩容操作时是不一样的,即HashTable的最大大小为Integer.MAX_VALUE - 8
问题4:HashTable默认扩容阈值是多少?加载因子默认值是多少?
根据解答上述几个问题,知道,HashTable默认扩容阈值为容量加载因子,而加载因子默认值为0.75,故默认扩容阈值=容量0.75
问题5:HashTable每次扩容大小是多少?
根据问题3中的源码,我们可以得出HashTable每次扩容大小是:
1、默认扩容的容量=原容量*2 + 1
2、如果扩容的容量比MAX_ARRAY_SIZE大,则设置为MAX_ARRAY_SIZE
问题6:HashTable hash函数是怎么实现的?
这个问题在问题7中一并回调
问题7:HashTable get的原理是什么?
我们先看get的源码:
public synchronized V get(Object key) {
// 当前散列表
HashtableEntry,?> tab[] = table;
// hash值,取的是key的hashcode()
int hash = key.hashCode();
// 通过(hash & 0x7FFFFFFF) % tab.length计算出当前key所对应的散列地址即下标
int index = (hash & 0x7FFFFFFF) % tab.length;
//根据下标取出链表,遍历链表找到匹配的值
for (HashtableEntry,?> e = tab[index] ; e != null ; e = e.next) {
//匹配的条件是hash相等,且节点key相等
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
通过源码,我们来总结下get的原理:
1、首先取key的hashcode()作为散列函数的值hash
2、通过对hash进行(hash & 0x7FFFFFFF) % tab.length求余操作,得到散列地址,即散列表的下标
3、根据求出的下标取出链表
4、遍历链表,判断链表中的节点是否匹配,匹配则返回节点的值
根据get源码我们回答下HashTable的hash函数实现是key.hashCode();
注意观察的同学会看到get操作中并没有对key进行空处理,那么如果key为null的话,会抛出NullPointerException异常
问题8:HashTable put的原理是什么?
先上源码:
public synchronized V put(K key, V value) {
// Make sure the value is not null
// value为空时直接抛出NullPointerException
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
HashtableEntry,?> tab[] = table;
// 取key的hashCode作为hash值
int hash = key.hashCode();
// 对hash进行(hash & 0x7FFFFFFF) % tab.length得到散列地址即下标
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//根据下标取得对应的记录即链表
HashtableEntry entry = (HashtableEntry)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;
}
private void addEntry(int hash, K key, V value, int index) {
//修改次数加1
modCount++;
//散列表
HashtableEntry,?> 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")
HashtableEntry e = (HashtableEntry) tab[index];
//将链表加入散列表中
tab[index] = new HashtableEntry<>(hash, key, value, e);
//大小加1
count++;
}
根据源码总结put原理如下:
1、先对要插入的值进行判空处理
2、先判断散列表中是否已经存在要插入的键值对:
2.1、对hash计算出散列地址即下标
2.2、根据下标取得链表
2.3、遍历链表,查找要插入的键值对
2.4、如果已经存在对应的节点,则直接更新值,并返回旧值
3、如果散列表中不存在要插入的键值对,则:
3.1、先判断是否需要进行扩容处理,如果需要则先进行扩容处理,再重新计算散列地址
3.2、接着创建新的节点作为链表的表头,再将链表加入散列表中
兄die,你细心观察了吗?key一样是不允许为null的哦,不然会抛出NullPointerException
问题9:HashTable 是否是线程安全?
如果你够细心,会发现上面的get、put源码中,方法都使用了synchronized修饰的,我们再来看下删除和清除操作:
public synchronized void putAll(Map extends K, ? extends V> t) {
}
public synchronized boolean remove(Object key, Object value) {
}
public synchronized void clear() {
}
public synchronized Object clone() {
}
其他读写操作的函数就不一一列举了,这些读写操作的函数都使用了同步内置锁synchronized修饰,也就是HashTable是线程安全类
问题10:HashTable与HashMap的区别是什么?
区别还是很大的,我们主要说它主主要的区别:
1、HashTable是线程安全类,而HashMap不是线程安全类,因为HashTable中的读写相关的操作均使用了同步内置锁进行修饰,而HashMap的读写操作并没有任何加锁、同步机制
2、HashTable是不允许key和value为null的,如果HashTable的key或value为null会抛出NullPointerException,而HashMap是允许key和value都为null的,所有相比而言HashMap更为健壮,不需要额外的处理null
3、HashTable默认的容量是11,而HashMap默认容量是16;HashTable阈值计算规则和HashMap的一样都是等于容量*加载因子;HashTable和HashMap的加载因子默认值都是0.75;HashTable最大容量是Integer.MAX_VALUE-8,而HashMap最大容量是Integer.MAX_VALUE
4、HashTable和HashMap都使用的是数组+单链表数据结构,不同的是HashTable使用的HashTableEntry作为节点,而HashMap使用Node作为节点
5、HashMap比HashTable性能更好,因为HashTable使用了同步内置锁,而HashMap没有;但是HashMap不是线程安全