JDK1.8Hashtable探索
本文的讨论分析是基于JDK1.8进行的
依旧是采用前几篇文章的大纲来进行介绍
1.简介
Hashtable 采用数组+单链表来实现的,Hashtable 实现了一个哈希表,它将键映射到值。任何非 null 对象可以用作键或值。为了成功存储和检索哈希表中的对象,用作键的对象必须实现 hashCode 方法和 equals 方法。Hashtable 的方法被synchronized修饰,因此是同步的、线程安全的。
2.探索
2.1类关系
Hashtable 继承了 Dictionary,能够重写里面的键值对应的一些方法,但是官方已经废弃它,推荐新的实现应该实现Map接口,而不是扩展这个类。
Hashtable 实现了 Map 接口,能够实现其中的所有可选的Map操作;
Hashtable 实现了 Cloneable 接口,能够使用 clone() 方法;
Hashtable 实现了 Serializable 接口,支持序列化操作。
2.2属性
/**
* 散列表数据
*/
private transient Entry<?,?>[] table;
/**
* 散列表中的条目总数
*/
private transient int count;
/**
* 临界值
* (这个字段的值是(int)(capacity * loadFactor)。)
*
* @serial
*/
private int threshold;
/**
* 加载因子
*
* @serial
*/
private float loadFactor;
/**
* 记录结构性变化
*/
private transient int modCount = 0;
/**版本序列号 */
private static final long serialVersionUID = 1421746759512286392L;
table 乍一看是个Entry[ ] 数组,其实也是个单向链表;
count 是记录了整个table的大小;
threshold 临界值或者阀值,是判断是否需要扩容的重要依据,具体计算为 threshold = capacity * loadFactor;
loadFactor 为加载因子
modCount 记录结构性变化,与fail-fast机制有关(后期专门写一篇介绍)。
2.3构造方法
Hashtable 一共有 4 个构造方法。
/**
* 用指定的初始容量和指定的加载因子构造一个新的空的散列表。
*
* @param initialCapacity 散列表的初始容量。
* @param loadFactor 散列表的加载因子。
*/
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];//初始化table数组
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化临界值
}
/**
* 用指定的初始容量和默认加载因子(0.75)构造一个新的空哈希表。
*
* @param initialCapacity the initial capacity of the hashtable.
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
* 使用默认的初始容量(11)和加载因子(0.75)构造一个新的空哈希表。
*/
public Hashtable() {
this(11, 0.75f);
}
/**
* 使用与给定Map相同的映射构造一个新的散列表。
* 散列表的初始容量足以容纳给定Map中的映射和默认加载因子(0.75)。
*
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);//初始化hashtable
putAll(t);//将t集合放入hashtable中
}
1)Hashtable()
默认初始容量为 11 ,加载因子为 0.75。
2)Hashtable(int initialCapacity)
用指定的初始容量和默认加载因子(0.75)构造一个新的空哈希表。
3)Hashtable(Map<? extends K, ? extends V> t)
使用与给定Map相同的映射构造一个新的散列表。
4)以上 3 个构造方法其实最后都是调用了Hashtable(int initialCapacity, float loadFactor)
构造方法。
5)Hashtable(int initialCapacity, float loadFactor)
在其中实现了 table 和 threshold 的初始化工作以及异常情况的判断。
2.4核心方法
(1)putAll( )
/**
* 将指定映射中的所有映射复制到此散列表。
* 这些映射将替换此散列表对当前指定映射中的任何键的任何映射。
*
*/
public synchronized void putAll(Map<? extends K, ? extends V> t) {
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())//遍历t集合
put(e.getKey(), e.getValue());//调用put方法
}
putAll()
中遍历集合调用 put()
方法。
(2)put(K key, V value)
/**
* 将指定的键映射到此散列表中指定的值
* 键和值都不能是 null
* 通过使用与原始键相等的键调用 get 方法,可以检索该值。
* 该方法是线程安全的,被synchronized修饰
*/
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;//初始化数组
int hash = key.hashCode();//计算hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//桶的位置索引
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {//如果桶中hash值相等且值相等
V old = entry.value;//获取旧值
entry.value = value;//覆盖旧值
return old;
}
}
addEntry(hash, key, value, index);//调用addEntry方法
return null;
}
1) put(K key, V value)
方法中会先初始化 table 数组,然后计算 key 对应的 hashcode() 以及 key 在桶中的位置索引 index。
2)如果放入的键值对都不为空,判断是否桶中 hash 和 key 相等的位置是否有值,有的话覆盖原值。
3)调用addEntry( )
方法。
private void addEntry(int hash, K key, V value, int index) {
modCount++;//结构性加1
Entry<?,?> tab[] = table;
if (count >= threshold) {//如果散列表中条目数大于临界值
// 如果超出临界值,则扩容
rehash();
tab = table;//初始化新值
hash = key.hashCode();//计算key的hash值
index = (hash & 0x7FFFFFFF) % tab.length;//key的位置索引
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];//初始化新的链表
tab[index] = new Entry<>(hash, key, value, e);//保存新链表到tab中
count++;//链表大小加1
}
1)如果 table 大小 count 大于临界值 threshold ,则进行扩容操作。
2)初始化新 tab 数组,key 的hash值,key 的位置索引。
3)保存新值到tab链表中,链表大小加1。
(3)rehash()扩容方法
/**
* 增加散列表的容量并在内部重新组织,以便更有效地容纳和访问条目。
* 当散列表中的键数超过散列表的容量和加载因子时,将自动调用此方法。
*/
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;//保存旧的table容量
Entry<?,?>[] oldMap = table;//保存旧的数组
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;//扩大容量为原来的2倍+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++;//结构性加1
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//计算新的临界值
table = newMap;//保存新的table
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;//产生新的hash位置
e.next = (Entry<K,V>)newMap[index];//同一表中,新结点链接到表头
newMap[index] = e;//保存新值
}
}
}
1 ) 首先保存旧的table容量和数组
2 ) 扩大容量为原来的2倍+1,判断是否需要重新赋值容量值
3 ) 计算新的临界值,保存table数组,把旧数组遍历保存到扩容后的数组中
(4)get(Object key)
/**
* 返回指定键映射到的值,
* 或者如果此映射不包含密钥的映射,则返回null。
*/
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;//赋值新数组链表
int hash = key.hashCode();//计算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)) {//如果hash值相同且key相等
return (V)e.value;//返回
}
}
return null;
}
1)先计算key的hashcode和key对应的索引位置。
2)遍历数组链表查找hash值和key都匹配的值。
(5)remove(Object key)
/**
* 从该散列表中删除键(及其相应的值)。
* 如果密钥不在散列表中,此方法不执行任何操作。
*/
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;//获取当前的table
int hash = key.hashCode();//计算要移除key的hashcode
int index = (hash & 0x7FFFFFFF) % tab.length;//计算位置
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];//保存该值到e中
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {//遍历链表
if ((e.hash == hash) && e.key.equals(key)) {//如果hash值且key相等
modCount++;//结构性加1
if (prev != null) {//如果不为空
prev.next = e.next;//下一个表头替换该位置
} else {//如果为空
tab[index] = e.next;//下一个表头赋值该位置
}
count--;//大小减1
V oldValue = e.value;//获取旧值
e.value = null;//置空当前位置值
return oldValue;
}
}
return null;
}
1)先获取hashcode和key的位置值 。
2)先保存垓值到e中 。
3)如果hash值和key都相等,表头不为空时,下一个表头替换该位置,表头为空时,下一个表头赋值该位置。
4)返回移除的值。
3.总结
- Hashtable 继承的是 Dictionary,HashMap 继承的是 AbstractMap,Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。AbstractMap实现了大部分的Map接口。
- Hashtable 的 put() 方法中是允许键和值为 null ,HashMap 则不允许为空。
- Hashtable 比 HashMap 多了 enumerator 迭代器。
- Hashtable 的大部分 public 方法都被 synchronized 修饰,是线程安全的,HashMap 不是,如果一个线程安全的实现是不需要的,建议使用 HashMap 代替 Hashtable。如果线程安全高度并发的实现是需要的,那么推荐使用java.util.concurrent.ConcurrentHashMap 或者 Collections.synchronizeMap 代替 Hashtable。