在咱们开讲源码之前,首先需要了解下什么是哈希表?
散列表(Hash table 又称哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度.这个映射函数就叫做散列函数,存放记录的数组叫做散列表. —— 百度百科
如图:
在Java中HashTable以数组
+链表
来实现,相对于HashMap来说要简单得多.HashTable不同于HashMap,它内部不允许插入null
值,同时它是线程安全的,所有的读写操作都进行了锁保护,但也难以避免的对读写效率产生了较大影响.因此在日常开发中为保证线程安全一般建议使用ConcurrentHashMap
.
为啥Java中
Hashtable
中的t
要小写? 这不符合驼峰命名规则啊
大意: Hashtable
创建于Java1,而集合的统一命名规范是后来在Java2中建立的,而当时又发布了新集合来代替它,再加上大量Java程序使用Hashtable
类,考虑到兼容问题不可能将Hashtable
改为HashTable
.同时Hashtable
已经过时了,不建议在代码中使用.
源码分析
结构图
继承关系
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
Dictionary
是JDK1.0里引入的抽象类,用于存储键/值对,作用和Map类似.注意Dictionary
类已经过时了,在实际开发中,可以通过实现Map
接口来完成存储键值对的功能.
类中属性
/** 内部维护了一个 Entry 数组 */
private transient Entry<?,?>[] table;
/** 哈希表里的元素数量 */
private transient int count;
/** 触发扩容的阈值 */
private int threshold;
/** 加载因子 默认 0.75 */
private float loadFactor;
/** 记录 涉及到结构变化的次数(offer/remove/clear等) */
private transient int modCount = 0;
/** 版本号 */
private static final long serialVersionUID = 1421746759512286392L;
table
数组里存的Entry
实际上是一个单向链表,哈希表的键值对都是存在table
里的.
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
...
}
构造函数
public Hashtable() { this(11, 0.75f); }
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }
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);
//初始容量最小为1
if (initialCapacity==0) initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
Hashtable的默认容量是11
,loadFactor
默认加载因子0.75
.threshold
为
数组容量 * loadFactor
.
核心函数
添加函数
public synchronized V put(K key, V value) {
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// &运算取正值,再取模计算位置
int index = (hash & 0x7FFFFFFF) % tab.length;
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 如果这个hash和key都已经存在了,就把原来的value替换掉
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;
}
put
函数通过关键字synchronized
保证了线程安全.第一行就表明了Hashtable中value都不能为null
.通过hash & 0x7FFFFFFF
来规避掉负数,再进行分配位置.如果key经常存在了,则覆盖旧值并返回旧值.核心通过addEntry
来添加元素.
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
// 若当前容量已经大于 阈值 进行rehash扩容
if (count >= threshold) { // threshold = count * loadFlor
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = (Entry<K,V>) tab[index];
// 最新插入的 排在最前面
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
addEntry
函数 先会判断如果需要扩容 当前数量 >= 阈值
,调用rehash
进行扩容,否则链表叠加.
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 新容量为老容量的一倍+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;
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;
}
}
}
rehash
函数先会把容量扩大1倍+1,然后创建一个newMap
,把oldMap
里的元素遍历复制到新的newMap
里,这个过程是比较耗时的,同时此操作后数组和链表里元素的位置都会发生改变.
删除函数
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
Entry<K,V> e = (Entry<K,V>)tab[index];
// 遍历链表 e 当前节点, prev 上一个节点 next 下一个节点
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
// 性能优化 先 进行hash 判断
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
// 若当前节点非头结点
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
remove
函数比较简单,通过key
定位在数组中的位置,再遍历链表删除元素.
获取函数
public synchronized V get(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 (V)e.value;
}
}
return null;
}
get
函数和remove
函数比较类似,都是先通过key
定位在数组中的位置,再迭代链表找到元素并直接返回.
迭代器
Hashtable
里迭代器使用的是比较老的Enumeration
接口,其作用和Iterator
类似,只提供了遍历的功能.虽然Enumeration
还未被废弃,但现在代码里已经很少使用了.本篇文章就不再讲述了,有兴趣的可以去翻翻代码~
public interface Enumeration<E> {
/** 判断是否还有元素 */
boolean hasMoreElements();
/** 如果还有元素则返回下一个元素,否则抛NoSuchElementException异常 */
E nextElement();
}
结语
Hashtable
整体是比较简单的,其内部充斥着大量的遍历操作,当数据量大的时候操作会非常耗时,非特殊情况下是用不到的.
一般来说,在日常开发中非并发场景推荐使用HashMap
,并发场景下虽然可以用Hashtable
,但是更推荐使用ConcurrentHashMap
.
ConcurrentHashMap
内部虽然也是使用Synchronized
,但它是针对单个对象的锁,相比于Hashtable
里锁的粒度更细,效率更高.