先去了解HashMap效果更佳【JDK源码】HashMap(一)
继承体系
Hashtable 的函数都是同步的,这意味着它是线程安全的,能用于多线程环境中。它的key、value都不可以为null。
Hashtable中的映射不是有序的。
Hashtable采用"拉链法"实现哈希表。
基本属性
/**
* 哈希表数据
*/
private transient Entry<?,?>[] table;
/**
* 哈希表中元素的总数
*/
private transient int count;
/**
* Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"
*/
private int threshold;
/**
* 加载因子
*/
private float loadFactor;
/**
* Hashtable被改变的次数,用于fail-fast机制的实现
* 所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你(你已经出错了)
*/
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);
//如果初始容量为0则让其等于1
if (initialCapacity==0)
initialCapacity = 1;
//初始化加载因子
this.loadFactor = loadFactor;
//创建一个Entry类型的数组长度为参数initialCapacity的值。
table = new Entry<?,?>[initialCapacity];
//计算阀值在初始容量乘以加载因子的值和后者(整数最大32位)之间取最小值。
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
/**
* 用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
* 默认构造函数,容量为11,加载因子为0.75。
*/
public Hashtable() {
this(11, 0.75f);
}
/**
* 根据传入的Map集合直接初始化HashTable,容量为传入Map集合容量大小*2与11进行比较,取值较大者
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
在初始化阶段,Hashtable就已经为table属性开辟了内存空间,这和HashMap中不同,HashMap是在第一次调用put方法时才为table开辟内存的。还有就是默认初始容量不同,一个16一个11。
put()
public synchronized V put(K key, V value) {
// Make sure the value is not null
// 发现值不能等于null 与hashmap不同
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
//确保该键不在哈希表中
//迭代table,将其赋值给局部变量tab
Entry<?,?> tab[] = table;
//计算key的hash值,确认在table[]中的索引位置
//这里如果key为null调用Object的hashCode方法就会报空指针异常,所以key也不能为null
int hash = key.hashCode();
// 计算索引位置,这里与hashmap不同
/**
* 0x7FFFFFFF: 16进制表示的整型,是整型里面的最大值
* 0x7FFFFFFF: 0111 1111 1111 1111 1111 1111 1111 1111:除符号位外的所有1
* (hash & 0x7FFFFFFF) 将产生正整数
* (hash & 0x7FFFFFFF) % tab.length 将在标签长度的范围内
* 所以hashtable采用奇数导致的hash冲突会比较少,采用偶数会导致的冲突会增多
* 4%2 = 6%2 = 8%2 ……
*/
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历对应index位置的链表
for(; entry != null ; entry = entry.next) {
//如果已经有了相同key的元素 ,则更新数据,返回原来的元素
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 增加新Entry元素
addEntry(hash, key, value, index);
return null;
}
- 我们可以看到put方法加了
synchronized
维护,所以这个方法是线程安全的 - 从计算hash就可以看出为什么采用11奇数作为初始容量
put
方法不允许key和value为null
值,如果发现是null
,则直接抛出异常addEntry
方法
addEntry()
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//元素个数大于阈值则将扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
//扩容后也找到index,也就是链表对应的索引
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];
//然后采用头插法将元素插入,这里和hashmap 1.8不同
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
- 是否扩容
- 插入头部,和hashmap1.8不同
rehash()
protected void rehash() {
//原来的容量(旧的数组的长度)
int oldCapacity = table.length;
//将原来的table保存起来
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新容量 原来两倍加一 保证奇数
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;
// 把旧哈希表的键值对重新哈希到新哈希表中去(双重循环,耗时!)
// 这个方法类似于HashMap扩容后,将旧数组数据赋给新数组,
// 在HashMap中会将其旧数组每个桶位进一步‘打散',放置到新数组对应的桶位上(有一个重新计算桶位的过程)
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;
//将每条链表的每个结点打乱重新hash 重新组建找到自己的位置,还是头结点插入
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
- 扩容2倍加1
- 更新阈值,更新table
- 遍历旧表中的节点,计算在新表中的index,插入到对应位置链表的头部
get()
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;
}
synchronized
,保证线程安全
remove()
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];
//找到删除结点的前结点prev
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
//e的前结点不为空
if (prev != null) {
//把e删除了
prev.next = e.next;
//e是头结点
} else {
//把e的下一个节点左头结点
tab[index] = e.next;
}
count--;
V oldValue = e.value;
//交给gc处理e
e.value = null;
//返回旧的值
return oldValue;
}
}
return null;
}
-
synchronized
,保证线程安全 -
存在就删除并返回删除的值,不存在返回null
Hashtable 和 HashMap的区别
- HashMap可以允许存在一个为null的key和任意个为null的value,但是HashTable中的key和value
都不允许为null
// HashTable
if (value == null) {
throw new NullPointerException();
}
int hash = key.hashCode();
============================
// HashMap
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- Hashtable的方法是
同步的
,而HashMap的方法不是。所以有人一般都建议如果是涉及到多线程同步时采用HashTable,没有涉及就采用HashMap。 - 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值
// HashTable
int hash = key.hashCode();
========================
// HashMap
int hash = hash(key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组
默认大小是11
,增加的方式是old*2+1
。HashMap中hash数组的默认大小是16
,而且一定是2的指数
。这与hash冲突有关