概述
HashTable存储的键值对,它的key和value都不可以为Null。
为了能成功的存储健值对,做为key的对象必须实现hashCode()和equals()方法。
HashTable实例有两个参数影响其性能,初始化容量和加载因子。初始容量是哈希表创建时的容量,注意HashTable的状态为Open。在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。
通常,默认加载因子(.75)在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。
初始容量主要控制空间消耗与执行 rehash 操作所需要的时间损耗之间的平衡。如果初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生 rehash 操作。但是,将初始容量设置太高可能会浪费空间。
如果很多条目要存储在一个 Hashtable 中,那么与根据需要执行自动 rehashing 操作来增大表的容量的做法相比,使用足够大的初始容量创建哈希表或许可以更有效地插入条目。
HashTable是线程安全的,但是如果在不需要同步的情况下使用HashMap,在高并发同步的情况下使用ConcurrentHashMap。
源码分析
全局变量
HashTable的内部数据载体是一个Entry数组
private transient Entry<?,?>[] table;
HashTable中Entry对象的总数量
private transient int count;
table是否要扩容的阈值,当Entry实体的数码超过了这个阈值,就需要对HashTable进行扩容。threshold=capcity*loadFactor
private int threshold;
加载因子loadFactory。。threshold=capcity*loadFactor,loadFactory意味着HashTable的容量越小,但是桶中的条目数量更多,这意味着查询条目需要花更多的时间,典型的时间换空间。如果loadFactor越小,则意味着要创建的桶的容量更大,相应的查询桶中条目就可以花更少的时间,典型的空间换时间。
private float loadFactor;
Hashtable内部结构变化的次数。包括添加、删除和扩容,都会导致Hashtable内部结构变化
构造方法
四个构造函数
/**
创建一个空的包含初始容量和指定加载因子的HashTable
*/
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);
}
/**
容量为11,默认加载因子0.75
*/
public Hashtable() {
this(11, 0.75f);
}
/**
根据原有集合创建Hashtable
/*
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
get()函数
获取key对应的Value,如果没有就返回为Null。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//获取key对应的hash值
int hash = key.hashCode();
/**
(hash & 0x7FFFFFFF)对桶的长度桶的容量tab.length取余,定位key具体位于哪个桶
0x7FFFFFFF = 2的32次方-1=1111111111111111111111111111111
*/
int index = (hash & 0x7FFFFFFF) % tab.length;
//遍历桶下的链表,查询对应的key的对象
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()函数
containsKey()函数的实现基本与get()函数一致,都是通过查询key对应的具体的桶,然后遍历该桶的链表。
public synchronized boolean containsKey(Object key) {
Entry<?,?> tab[] = table;
//获取key对应的hash值
int hash = key.hashCode();
//(hash & 0x7FFFFFFF)对桶的长度取余,定位数据具体位于哪个桶
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 true;
}
}
return false;
}
containsValue()函数
根据值获取对应的keys,多个key对应一个value的情况,返回的key有多个。这个方法查询的代价比containsKey
昂贵得多,因为要遍历所有的桶,同时要遍历桶的链表一一比较值。
public boolean containsValue(Object value) {
return contains(value);
}
/**
同步调用
*/
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//遍历所有的桶
for (int i = tab.length ; i-- > 0 ;) {
//遍历桶下所有的链表
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
put()函数
添加新的Entry,首先定位到桶,然后再遍历桶的链表,如果存在则直接返回,不存在则添加。
/**
添加键值对,key和value都不允许为空值
*/
public synchronized V put(K key, V value) {
//确保净值对的值不能为空
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//获取key的hash值,如果key为空,则抛出空指针异常,所以key也不能为空
int hash = key.hashCode();
//获取hash值为具体的哪个桶
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历桶中的链表,如果已经含有该key,则返回key对应的value,不再加入
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
/**
添加entry
*/
private void addEntry(int hash, K key, V value, int index) {
//因为添加了Entry,则Hashtable的结构体修改,则modeCount加1
modCount++;
Entry<?,?> tab[] = table;
//如果桶中所有的Entry总数超过了阈值,则需要对桶进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
//获取key对应的hash值
hash = key.hashCode();
//hash值对应的哪个桶
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
//加Entry加入到链表中
tab[index] = new Entry<>(hash, key, value, e);
//节点数+1
count++;
}
putAll()函数
该函数将传入的Map对象复制到新的Hashtable,循环调用以上函数put()
public synchronized void putAll(Map<? extends K, ? extends V> t) {
//循环遍历Map,将键值对复制到新的Hashtable
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}
rehash()函数
/**
扩容
*/
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新的容量扩大2倍+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;
}
//创建新的tab
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//设置阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
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;
//在这里会重新计算e位于哪个桶中
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//将e插入到首个节点的前面(该首节点与e位于相同的桶中)
e.next = (Entry<K,V>)newMap[index];
//即将e设置为首节点
newMap[index] = e;
}
}
}
1、首先先将当期容量扩大2倍+1
2、如果新的容量超过了最大值MAX_ARRAY_SIZE,而且当前容量已经超过了最大值MAX_ARRAY_SIZE,则不再扩容直接返回
3、如果新的容量超过了最大值MAX_ARRAY_SIZE,而且当前容量小于最大值MAX_ARRAY_SIZE,则将新的容量设置为最大值MAX_ARRAY_SIZE
4、设置新的容量阈值为Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1)
5、创建新的桶数组tab[],同时将旧桶的数据全部复制到新桶中,而且每个hash值要重新计算为于新桶的哪个位置,这个代价比较昂贵。
看一下容量扩张后的数组复制的一个实例
由实例我们也能得知,扩张后,hash值的位于哪个桶是重新计算的,同时新加入的节点是在原来的链表的首节点前插入,从这里我们还可以得知Hashtable的顺序是变化的。
remove()函数
/**
删除节点
*/
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
//获取key的hash值
int hash = key.hashCode();
//计算索引,(hash & 0x7FFFFFFF)对tab的容量取余,
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不为空,
将prev的下一节点设置为删除节点e的下一节点
*/
prev.next = e.next;
} else {
/**
删除节点的上一节点为空,则说明删除的是首节点
将删除节点的下一节点设置为首节点
*/
tab[index] = e.next;
}
//节点数减1
count--;
V oldValue = e.value;
e.value = null;
//返回删除节点的value
return oldValue;
}
}
//未找到删除的节点,返回为空
return null;
}
clear()函数
清空Hashtable
//清除Hashtable,
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
//遍历Hashtable所有的桶,将所有的桶清空
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;
}
clone()
克隆,这里的克隆方法是浅复制,Hashtable的结构体会被复制,键值对如果是指向型对象是不复制的。
public synchronized Object clone() {
try {
//复制Hashtable结构体
Hashtable<?,?> t = (Hashtable<?,?>)super.clone();
t.table = new Entry<?,?>[table.length];
for (int i = table.length ; i-- > 0 ; ) {
t.table[i] = (table[i] != null)
//复制链表结构体
? (Entry<?,?>) table[i].clone() : null;
}
t.keySet = null;
t.entrySet = null;
t.values = null;
t.modCount = 0;
return t;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
结合一个实例看下Hashtable的克隆
package com.leadbank.faas.income.test;
import java.util.Hashtable;
public class HashTableTest {
static Hashtable<String, Person> table = new Hashtable<String, Person>();
static class Person{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Person p = new Person();
p.setName("张三20");
p.setAge(20);
table.put("p", p );
System.out.println("修改前table="+table.toString());
Hashtable<String, Person> copyTab = (Hashtable) table.clone();
System.out.println("修改前copyTab="+copyTab);
Person p2 = (Person) table.get("p");
p2.setAge(21);
p2.setName("张三21");
System.out.println("修改后table="+table.toString());
System.out.println("修改后copyTab="+copyTab);
}
}
执行结果:
修改前table={p=Person [name=张三20, age=20]}
修改前copyTab={p=Person [name=张三20, age=20]}
修改后table={p=Person [name=张三21, age=21]}
修改后copyTab={p=Person [name=张三21, age=21]}
如果是深度克隆,修改后coptyTable的Person信息不应该跟随table的信息而变化。
Enumerator
枚举器,elements()、keys()、entrySet()的迭代器功能都需要使用该枚举器Enumerator
/**
枚举生成器。该枚举生成器实现了Enumeration和Iterator接口,禁用
迭代器方法创建单独的实例。这对于避免无意中增加通过枚举接口
授予用户能力是很有必要的。
*/
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
//当前Hashtable对象的桶
Entry<?,?>[] table = Hashtable.this.table;
//桶的当前容量
int index = table.length;
Entry<?,?> entry;
//遍历期间,上一次的值
Entry<?,?> lastReturned;
//0:返回key值,1:返回value,2:返回键和值
int type;
/**
true ->使用迭代器
false->使用枚举
迭代器比枚举类多了删除功能,而且方法名也做了修改,
*/
boolean iterator;
/**
modCount:Hashtable内部结构变更的次数,
expectedModCount:迭代器创建时候初始化的modCount的值。如果Hashtable结构因为
除使用迭代器以外的方法使Hashtable内部结构发生了变动(modCount变大),这个
时候modCount与expectedModCount是不相等的,一旦不相等,则这个迭代器就失效了,
不能再使用了。其实这个就是java集合的fail-fast失败机制。当多个线程对同一个
集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过
iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A
访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件
如果 expectedModCount
*/
protected int expectedModCount = modCount;
Enumerator(int type, boolean iterator) {
this.type = type;
this.iterator = iterator;
}
/**
判断该tab是否还有更多的节点。该方法每调用一次,
就会判断当前节点是否为空,如果为空,就会上移一个桶,
直到遍历完所有的桶(i==0),桶的遍历从最大的桶开始。
而对应到具体桶的链表的节点的位置移动则交给nexElement()方法。
*/
public boolean hasMoreElements() {
Entry<?,?> e = entry;
int i = index;
Entry<?,?>[] t = table;
/* Use locals for faster loop iteration */
while (e == null && i > 0) {
e = t[--i];
}
//将当前节点e赋值给共有元素entry,以便nextElement()方法使用
entry = e;
//将当前桶位置赋值给共用index,以便nextElement()方法使用
index = i;
//当前节点为空,则表示没有更多的非空节点
return e != null;
}
@SuppressWarnings("unchecked")
/**
获取下一个节点元素
*/
public T nextElement() {
//将共有元素entry赋值给当前节点et
Entry<?,?> et = entry;
//当前迭代器指针所在桶的位置
int i = index;
Entry<?,?>[] t = table;
/**
如果当前节点为空,则获取上一个桶。如果外部
调用了hasMoreElements()方法来判断,则这个基本
不会发生。
*/
while (et == null && i > 0) {
et = t[--i];
}
//将当前节点et赋值给共有元素entry
entry = et;
将当前桶位置赋值给共用index
index = i;
//如果et不为空
if (et != null) {
Entry<?,?> e = lastReturned = entry;
entry = e.next;//下一个节点
//根据type返回键还是值,还是键值对
return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
}
throw new NoSuchElementException("Hashtable Enumerator");
}
// 迭代器方法,内部调用枚举的方法
public boolean hasNext() {
return hasMoreElements();
}
// 迭代器方法,内部调用枚举的方法
public T next() {
//fail-fast事件发生
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return nextElement();
}
/**
删除当前节点lastReturned,即刚刚调用next()方法的节点,
使用remove()必须调用过next()方法来获取最近返回的节点
*/
public void remove() {
/**
如果用户使用的枚举,不是迭代器,则抛出异常,迭代器
才拥有remove方法,枚举没有remove()方法
*/
if (!iterator)
throw new UnsupportedOperationException();
//如果最近返回的lastReturned为空,则没有可以删除的节点
if (lastReturned == null)
throw new IllegalStateException("Hashtable Enumerator");
//fail-fast事件发生,当前迭代器失效不可用
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//删除的时候以Hashtable对象为锁
synchronized(Hashtable.this) {
Entry<?,?>[] tab = Hashtable.this.table;
//删除节点为于哪个桶中
int index = (lastReturned.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 == lastReturned) {
modCount++;
expectedModCount++;
if (prev == null)
tab[index] = e.next;
else
prev.next = e.next;
count--;
lastReturned = null;
return;
}
}
throw new ConcurrentModificationException();
}
}
}
keySet()
创建KeySet,KeySet拥有迭代器iterator,该迭代器iterator通过上文提到Enumerator来实现,然后用户就可以通过迭代器来访问tab内部数据了。当然KeySet本身也可以不通过迭代器,调用外部方法来实现查询和删除等功能的。
public Set<K> keySet() {
if (keySet == null)
//创建线程安全的结合Set
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
private class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return getIterator(KEYS);
}
public int size() {
return count;
}
//不同过迭代器,用外部的方法查询是否否包含该对象o
public boolean contains(Object o) {
return containsKey(o);
}
//不通过迭代器,调用外部的删除方法删除对象o
public boolean remove(Object o) {
return Hashtable.this.remove(o) != null;
}
//不通过迭代器,嗲用外部清除方法清除tab
public void clear() {
Hashtable.this.clear();
}
}
values()函数
该函数返回ValueCollection类型的value对象,该对象包含迭代器iterator,代器iterator通过上文提到Enumerator来实现,然后用户就可以通过迭代器来访问tab内部数据了。
public Collection<V> values() {
if (values==null)
values = Collections.synchronizedCollection(new ValueCollection(),
this);
return values;
}
private class ValueCollection extends AbstractCollection<V> {
public Iterator<V> iterator() {
return getIterator(VALUES);
}
public int size() {
return count;
}
public boolean contains(Object o) {
return containsValue(o);
}
public void clear() {
Hashtable.this.clear();
}
}
最后看几个遍历集合的方法实例:
package com.leadbank.faas.income.test;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
public class HashtableTest2 {
static Hashtable<String, String> hashTable = new Hashtable<String, String>();
public static void main(String[] args) {
//声明BiConsumer类型的action
hashTable.put("key1", "value1");
hashTable.put("key2", "value2");
hashTable.put("key3", "value3");
//遍历key值方法
//1、通过枚举Enumeration遍历集合的key
System.out.println("-----------------枚举遍历key------------------------");
Enumeration<String> keyNuneration = hashTable.keys();
while(keyNuneration.hasMoreElements()){
String key = keyNuneration.nextElement();
System.out.println("key="+key);
}
//2、通过迭代器遍历集合的key
System.out.println("-----------------迭代器遍历key------------------------");
Set<String> keySet = hashTable.keySet();
Iterator<String> keyIter = keySet.iterator();
while(keyIter.hasNext()){
String key = keyIter.next();
System.out.println("key="+key);
}
//2.1、补充一下函数式编程方式
System.out.println("-----------------函数式编程遍历key------------------------");
Consumer<String> action = (key) -> System.out.println("key="+key);
keySet.forEach(action);
//遍历Value值方法
//1、通过Enumeration遍历集合的Value值
System.out.println("-----------------枚举遍历value------------------------");
Enumeration<String> vEnumeration = hashTable.elements();
while(vEnumeration.hasMoreElements()){
String value = vEnumeration.nextElement();
System.out.println("value="+value);
}
//2、通过迭代器Iterator遍历集合集合的Value值
System.out.println("-----------------迭代器遍历value------------------------");
Collection<String> collection = hashTable.values();
Iterator<String> collectionIter = collection.iterator();
while(collectionIter.hasNext()){
String value = collectionIter.next();
System.out.println("value="+value);
}
//通过迭代器遍历集合的key-value
System.out.println("-----------------迭代器遍历key-value------------------------");
Set<Map.Entry<String, String>> set = hashTable.entrySet();
Iterator<Entry<String, String>> kvIter = set.iterator();
while(kvIter.hasNext()){
Map.Entry<String, String> entry = kvIter.next();
System.out.println("key="+entry.getKey()+"->value="+entry.getValue());
}
//2、补充一个函数式编程
System.out.println("-----------------函数式编程遍历key-value------------------------");
Consumer<Map.Entry<String, String>> biAction = (entry) ->{
System.out.println("key="+entry.getKey()+"->value="+entry.getValue());
};
set.forEach(biAction);
}
}
输出结果:
-----------------枚举遍历key------------------------
key=key3
key=key2
key=key1
-----------------迭代器遍历key------------------------
key=key3
key=key2
key=key1
-----------------函数式编程遍历key------------------------
key=key3
key=key2
key=key1
-----------------枚举遍历value------------------------
value=value3
value=value2
value=value1
-----------------迭代器遍历value------------------------
value=value3
value=value2
value=value1
-----------------迭代器遍历key-value------------------------
key=key3->value=value3
key=key2->value=value2
key=key1->value=value1
-----------------函数式编程遍历key-value------------------------
key=key3->value=value3
key=key2->value=value2
key=key1->value=value1