继承关系
Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。
同时也可以被克隆和序列化。
底层结构
HashTable类中,保存实际数据的,依然是Entry对象。其数据结构与HashMap是相同的。
也是数组加链表
构造方法和初始化
一些基本属性也与hashmap类似
基本属性
private transient Entry<?,?>[] table;
/**
* The total number of entries in the hash table.
*/
private transient int count;
/**
* The table is rehashed when its size exceeds this threshold. (The
* value of this field is (int)(capacity * loadFactor).)
*
* @serial
*/
private int threshold;
/**
* The load factor for the hashtable.
*
* @serial
* 默认0.75
*/
private float loadFactor;
/**
* 修改的次数
*/
private transient int modCount = 0;
构造方法
1.传入初始化大小和负载因子的构造方法
初始大小和负载因子都有相应的限制
同时计算出阈值
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);
}
2.传入初始化大小的构造方法
默认加载因子为0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
3.默认构造方法
初始大小为11,加载因子为0.75
public Hashtable() {
this(11, 0.75f);
}
4.传入一个map映射的构造方法
会进行容器初始化大小的计算
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
常用api
put
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数组
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// 通过hash值与table长度取余,确定元素的位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 取出当前位置上的元素
Entry<K,V> entry = (Entry<K,V>)tab[index];
//如果存在,则获取对应下标的数组进行循环比较
for(; entry != null ; entry = entry.next) {
// 查找是否具有相同hash值和key的元素,有则替换,并返回
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 操作
1.确保value值不为空值
2.确保 key不存在hashtable中,如果存在就替换后返回该对象。
3.不存在直接调用addEntry方法,进行值的放入。
源码可以看出Hashtable是不允许key,value为null。
流程图
addEntry方法
源码解读:
private void addEntry(int hash, K key, V value, int index) {
//记录变化次数
modCount++;
//定义并赋值entry数组
Entry<?,?> tab[] = table;
//如果hashtable的entry数量大于了阈值,则进行扩容
//threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
//扩容
rehash();
//扩容后重新对tab赋值
tab = table;
//重新计算元素的位置
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
// 取出当前位置上的元素
Entry<K,V> e = (Entry<K,V>) tab[index];
// 进行插入操作,从这里可以看出新的元素总是在链表头的位置
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
总结
在该函数中涉及扩容,但是由于put操作为线程安全,所以扩容时也是线程安全的。
扩容要求:当前元素个数大于等于容量与扩容因子的乘积。
还有一点需注意插入的新节点总是在链表头。
流程图
rehash
源码解读:
@SuppressWarnings("unchecked")
protected void rehash() {
//获取旧的容量大小
int oldCapacity = table.length;
//定义赋值旧的map
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//计算新的容量大小,为旧容量的两倍再+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;
}
//初始化新的map,传入容量大小
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//记录集合的变化次数
modCount++;
//计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//赋值table
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;
}
}
}
流程图
总结
1.新的数组大小是原来的2倍加1,
2.扩容时插入新元素采用的是头插法,元素会进行倒序
3.关键的两重循环
它先去从后向前的遍历数组,然后第二层循环去遍历数组中每个对象链表,重新计算链表中每一个节点的位置,这样两层循环下来,各个节点的位置就调整好了,容器容量也扩展
get
源码解读: synchronized修饰线程安全
@SuppressWarnings("unchecked")
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) {
// 遍历 寻找hash和key相同的元素
if ((e.hash == hash) && e.key.equals(key)) {
//有,返回
return (V)e.value;
}
}
//不存在,就返回null
return null;
}
流程图
总结
通过hash值与key进行查找,找到立即返回,未找到则返回null。
remove
源码分析: synchronized修饰线程安全
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
//通过key计算节点存储下标
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//循环遍历链表,通过hash和key判断键是否存在
//如果存在,直接将该节点设置为空,并从链表上移除
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;
}
}
//没有删除就返回null
return null;
}
流程图
总结
通过key计算阶段存储的下标,根据下标获取对应的链表,循环,寻找hash和key都相等的键,如果存在,就将该节点设置为空,并在链表中删除,返回删除的值。
不存在,就返回空值
特点
线程安全的原因
所有方法都有synchronized修饰,保证多线程下的线程安全。
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,
当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,
这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,
只有等待这个方法执行完毕或者代码块执行完毕,
这个线程才会释放该对象的锁,
其他线程才能执行这个方法或者代码块。
如何实现的线程安全
使用synchronized修饰
hashTable 和hashMap的区别
1、虽然 HashMap 和 Hashtable 都实现了 Map 接口,但 Hashtable 继承于 Dictionary 类,而 HashMap 是继承于 AbstractMap;
2、HashMap 可以允许存在一个为 null 的 key 和任意个为 null 的 value,但是 HashTable 中的 key 和 value 都不允许为 null;
3、Hashtable的方法是同步的,因为在方法上加了 synchronized 同步锁,而 HashMap 是非线程安全的;
4.HashMap的初始化大小为16,扩容是原来的2倍,而Hashtable的初始化大小是11,扩容是原来的2倍+1。
5.Hashtable扩容转移元素时采用的是头插法。HashMap在1.7的时候也是头插法,但是1.8改为尾插法了。
总结
如果想要快速用hashmap,如果要线程安全使用ConcurrentHashMap。
在实际运用中HashTable已经被淘汰了。
写在后面的话
每当你做出选择的时候,同时也以为着失去了另外一种可能性。
那么你每天都在失去,所以不要去纠结对错得失,既然选择了当下这条路。
那么剩下需要考虑的就是如何把这条路走好走宽。