HashTable源码解析(JDK1.8)
HashTable是一个数组+链表实现的线程安全的散列表
同样我们还是先上类的关系图
从类的关系图中可以看出HashTable继承一个抽象类和实现了三个接口,然后分别简单介绍一下:
- Dictionary:这里主要提供增删改查、keys集合查询、elements集合查询等的相关操作
- Map:提供队首、队尾增删改查等操作
- Cloneable:按字段复制操作
- Serializable:启用其序列化功能操作
属性
属性相关的源码
private transient Entry<?,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
private transient Entry[] table;//存储数据的
private transient int count;//存放元素的数量
private int threshold;//扩容的阀值
private float loadFactor;//负载因子
private transient int modCount = 0;//修改的次数
构造方法
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;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 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);
}
从四个构造方法中可以看出主要是initialCapacity、loadFactor、threshold这三个的不同参数
initialCapacity:初始容量,默认值采用的是11
loadFactor:负载因子,默认值采用的是0.75
threshold:HashTable所能容纳的最大数据量。threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
HashTable中的节点
HashTable中的节点对象是一个存储hash值,key,value以及下一个节点
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();
}
}
增删改查
插入元素/更新元素
其中0x7FFFFFFF是INT_MAX,转化为二进制为0111 1111 1111 1111 1111 1111 1111 1111
//synchronized可以看出,是线程安全的
public synchronized V put(K key, V value) {
// Make sure the value is not null value不能为null,要不然报空指针异常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//得到key的hash值
int hash = key.hashCode();
//计算得到index下标
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//找到数组中index对应的链表的节点
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
//判断如果当前节点的hash和要拆入的key的hash值一样,并且key一样则是更新元素,然后更新节点的value值,然后返回老值
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//没有找到相同hash值和key的则是拆入元素,我们来分析addEntry怎么实现拆入的
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
//修改次数modCount+1
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")
//拿到数组中index位置的节点
Entry<K,V> e = (Entry<K,V>) tab[index];
//HashTable采用的头插法,设置数组index位置新的节点,并设置该节点相关的hash、key、value以及下一个节点是原先这个位置的节点
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;
//扩容之后的新容量-最大容量>0
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//老容量等于MAX_ARRAY_SIZE最大值,则无法扩容老
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
//扩容之后的新容量=最大容量
newCapacity = MAX_ARRAY_SIZE;
}
//新数据数组初始化
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//修改次数+1
modCount++;
//新的阀值计算
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//数据赋值
table = newMap;
//从老数据的长度位置开始,去循环
for (int i = oldCapacity ; i-- > 0 ;) {
//取老数据i位置的节点,判断节点不为null,重新复制数据到新的newMap数组中去
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;
}
}
}
总结一下:根据的key去取hash,然后取模运算得到index,然后判断一下这个index对应的链表里的节点是否和当前插入的key、hash值都一样,都一样就是更新元素;不一样就是插入元素,插入元素的话需要判断一下是否需要扩容,需要的扩容的话就重新计算找到新的index下标设置新数据,最后再插入新数据到对应位置。putAll()、putIfAbsent()原理类似
删除元素
删除很简单,得到key相关的hash值,及其对应的index,然后找到数组中index中的节点,然后顺着链表往下找,找到了进行和链表一样的删除就行了
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
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;
}
查找元素
查找也很简单,得到key相关的hash值,及其对应的index,然后找到数组中index中的节点,然后遍历链表
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;
}
其他方法
containsKey()、contains()、clear()这些都是很简单的,都是遍历查找和遍历赋值为null
线程安全问题
-
HashTable通过synchronized实现了线程安全,但是有一定的局限性,加锁和释放锁的时候开销大,在组合操作的时候是线程不安全的
-
ConcurrentHashMap也是线程安全的,这个是找到对应的节点,对对应index位置上的链表或者红黑树进行加锁来保证线程安全的,范围更小,影响更小。put()、remove()是线程安全的,get()不是线程安全的,也不需要