借鉴:https://www.cnblogs.com/jilodream/p/7209021.html
https://www.cnblogs.com/skywang12345/p/3310887.html
说明: 在详细介绍Hashtable的代码之前,我们需要了解:和Hashmap一样,Hashtable也是一个散列表,它也是通过“拉链法”解决哈希冲突的。
Hashtable数据存储数组
private transient Entry[] table;
数据节点Entry的数据结构
private static class Entry<K,V> implements Map.Entry<K,V> {
//Entry数据类型的属性
int hash;//哈希码
K key;//键
V value;//值
Entry<K,V> next;// 指向的下一个Entry,即链表的下一个节点
// 构造函数
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//这个方法是复制哈希表自身的结构,并不复制键和值
protected Object clone() {
//返回这个副本,最后一个属性要判断是否为空,为空就复制空,否则复制下个结点的自身结构
return new Entry<K,V>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
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;
}
// 覆盖equals()方法,判断两个Entry是否相等。
// 若两个Entry的key和value都相等,则认为它们相等。
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
//返回语句种第一句意思是,
//如果这个键值为空,那么判断传入的对象键值是否为空,为空则说明相等,不为空则比较key的值
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
//覆盖hashcode方法
public int hashCode() {
return hash ^ (value==null ? 0 : value.hashCode());
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
Hashtable的方法
Hashtable中的变量
//Entry类型的数组
private transient Entry[] table;
// Hashtable中元素的实际数量
private transient int count;
// 阈值,用于判断是否需要调整Hashtable的容量(threshold = 容量*加载因子)
private int threshold;
// 加载因子
private float loadFactor;
// Hashtable被改变的次数
private transient int modCount = 0;
// 序列版本号
private static final long serialVersionUID = 1421746759512286392L;
clear()方法
//将此哈希表清空,使其不包含任何键。
public synchronized void clear() {
Entry tab[] = table;
modCount++;//表示hashtable被改变的次数
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;//表示hashtable中实际的元素
}
contains() 和 containsValue()
contains() 和 containsValue() 的作用都是判断Hashtable是否包含“值(value)”
public boolean containsValue(Object value) {
return contains(value);
}
public synchronized boolean contains(Object value) {
// Hashtable中“键值对”的value不能是null,
// 若是null的话,抛出异常!
if (value == null) {
throw new NullPointerException();
}
// 从后向前遍历table数组中的元素(Entry)
// 对于每个Entry(单向链表),逐个遍历,判断节点的值是否等于value
Entry tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {//循环整个数组找到元素,
if (e.value.equals(value)) {//判断元素的value是否相等
return true;
}
}
}
return false;
}
containsKey()
containsKey() 的作用是判断Hashtable是否包含key
public synchronized boolean containsKey(Object key) {
Entry tab[] = table;//先将table数记录副本
int hash = key.hashCode();//求得key得hash值
// 计算索引值,
// % tab.length 的目的是防止数据越界
int index = (hash & 0x7FFFFFFF) % tab.length;
// 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//这里不需要像ContainsValue一样需要遍历整个数组,因为能通过哈希码找到索引
if ((e.hash == hash) && e.key.equals(key)) {//如果两个entry对象的hashcode相等,且key的值相等则相等
return true;
}
}
return false;
}
get()
get() 的作用就是获取key对应的value,没有的话返回null
public synchronized V get(Object key) {
Entry tab[] = table;//记录存储数组
int hash = key.hashCode();//求keyhash值
// 计算索引值,
int index = (hash & 0x7FFFFFFF) % tab.length;
// 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素
//这里不需要遍历数组每个元素,因为能直接找到索引值
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
//如果entry的hash值相等,且key值相等(hash有可能key值不同,因为一个索引可能存了多个entry),则证明找到了这个entry
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
put()
让Hashtable对象可以通过put()将“key-value”添加到Hashtable中。
public synchronized V put(K key, V value) {
// Hashtable中不能插入value为null的元素!!
if (value == null) {
throw new NullPointerException();
}
// 若“Hashtable中已存在键为key的键值对”,
// 则用“新的value”替换“旧的value”
Entry tab[] = table;
int hash = key.hashCode();//求这个key的hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//通过key的hash值找到存这个key的entry的索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍历在这个索引的链表中
if ((e.hash == hash) && e.key.equals(key)) {//如果entry的hash相同且key相同则说明有同样元素
V old = e.value;//替换value
e.value = value;
return old;//返回旧值
}
}
//调用addEntry方法存入元素
addEntry(hash, key, value, index);
return null;
}
addEntry(hash, key, value, index);
return null;
}
putAll()
将“Map(t)”的中全部元素逐一添加到Hashtable中
public synchronized void putAll(Map<? extends K, ? extends V> t) {
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}
remove()
remove() 的作用就是删除Hashtable中键为key的元素
public synchronized V remove(Object key) {
Entry tab[] = table;//记录存储entry的数组
int hash = key.hashCode();//求键的hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash值求得索引
//遍历找到的索引位置的链表,prev是记录前驱结点,为了删除完后连接用
for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {//判断是否找到
modCount++;//操作次数加1
if (prev != null) {//说明不是第一个结点
prev.next = e.next;//把要删结点的下一个赋给prev的下一个
} else {//说明是第一个结点
tab[index] = e.next;//把这个结点赋给指定索引位置的第一个链表结点(第一个链表结点在数组中存储)
}
count--;//元素数量-1
V oldValue = e.value;
e.value = null;//值变为空
return oldValue;//输出旧值
}
}
return null;
}
Hashtable实现的Cloneable接口
Hashtable实现了Cloneable接口,即实现了clone()方法。
clone()方法的作用很简单,就是克隆一个Hashtable对象并返回。
// 克隆一个Hashtable,并以Object的形式返回。
public synchronized Object clone() {
try {
//先进行克隆
Hashtable<K,V> t = (Hashtable<K,V>) super.clone();
//因为是数组类型,所以进行深克隆,防止克隆的对象改变原对象
t.table = new Entry[table.length];
//依次赋值
for (int i = table.length ; i-- > 0 ; ) {
t.table[i] = (table[i] != null)
? (Entry<K,V>) table[i].clone() : null;
}
//key的集合设置为空
t.keySet = null;
//key-value的集合设置为空
t.entrySet = null;
//value的集合设置为空
t.values = null;
//将对象改变的次数设置为空
t.modCount = 0;
return t;
} catch (CloneNotSupportedException e) {//不支持克隆异常
throw new InternalError();
}
}
rehash方法:
这个方法是一个受保护方法。会在接下来的,hashtable添加元素的场景中被调用。他的作用呢,就是重新申请一块大小合适的内存。然后将键值元素重新安置到这块元素中。
1、计算新内存的大小。
2、计算元素在新table中的位置。
protected void rehash() {
//记录原数组大小
int oldCapacity = table.length;
//记录原数组
Entry<?,?>[] oldMap = table;
//给旧的长度乘2+1,变为新的长度
int newCapacity = (oldCapacity << 1) + 1;
//如果新的长度大于hashtable的最大长度
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//如果旧的长度已经是最大长度
if (oldCapacity == MAX_ARRAY_SIZE)
//那么不扩容直接返回
return;
//如果旧的长度没有达到最大,那么将hashtakbe最大长度赋值给新的长度
newCapacity = MAX_ARRAY_SIZE;
}
//建立新的map并且长度为上面扩容过的长度
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//操作次数++
modCount++;
//更新阈值,阈值是新长度乘加载因子和最大长度+1最小值
//新长度乘加载因子很简单,但是最大长度+1是如果新长度乘加载因子大于了最大长度
//那么阈值设置多少都没意义还是会停在hashtable最大值处,所以取他俩最小值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//把newmap赋值给table
table = newMap;
//通过双重循环来将旧的索引位置赋值给扩容后的索引位置(替换)
//第一个循环遍历旧数组所有元素
for (int i = oldCapacity ; i-- > 0 ;) {
//第二个循环遍历第一个循环传来每个数组元素的链表
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
//将旧值(要改变位置的结点)记录为e
Entry<K,V> e = old;
//将old前移(因为改变了这个结点的前驱结点的索引位置)
old = old.next;
获得旧值e的新的索引该存的位置
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//将之前已经存在在这个位置的元素变为要拆入的新节点的下一个结点
e.next = (Entry<K,V>)newMap[index];
//将e变为这个数组元素链表的第一位
newMap[index] = e;
}
}
}
添加元素的私有方法addEntry:
private void addEntry(int hash, K key, V value, int index) {
//操作次数加1
modCount++;
//记录entry数组
Entry<?,?> tab[] = table;
//加入新的结点后如果大于阈值,则扩展
if (count >= threshold) {
//扩展阈值
rehash();
//更新为扩展后的数组
tab = table;
//无用的步骤
hash = key.hashCode();
//更新为扩展后的索引位置
index = (hash & 0x7FFFFFFF) % tab.length;
}
//把新的结点应放位置原有第一个元素保存到e里面
Entry<K,V> e = (Entry<K,V>) tab[index];
//Entry的构造方法里最后一个放的是当前结点的next
//我们把e放在这里相当于,把新结点放在了第一位,而e成为了它的后继
tab[index] = new Entry<>(hash, key, value, e);
//元素数量加1
count++;
}
toString方法:
public synchronized String toString() {
//取得map的大小(因为是数组,从0开始索引,所以减1)
int max = size() - 1;
//如果集合为空,返回“{}”
if (max == -1)
return "{}";
//用这个字符串来显示要输出的值
StringBuilder sb = new StringBuilder();
//遍历整个Map的元素
Iterator<Map.Entry<K,V>> it = entrySet().iterator();
//输出前是“{”
sb.append('{');
//遍历所有元素
for (int i = 0; ; i++) {
Map.Entry<K,V> e = it.next();
K key = e.getKey();
V value = e.getValue();
//如果键存的是自己就输出this Map,防止出现无限递归
sb.append(key == this ? "(this Map)" : key.toString());
sb.append('=');
//如果值存的是自己就输出this Map,防止出现无限递归
sb.append(value == this ? "(this Map)" : value.toString());
//当遍历完输出“}”
if (i == max)
return sb.append('}').toString();
//每次遍历+","
sb.append(", ");
}
}