您的位置:首页 > 编程语言 > Java开发

07.Java 集合 - HashTable

2016-06-22 22:02 639 查看

基本概念

1.结构

首先来看它的继承结构:



再来看看它的结构图,HashTable 是基于哈希表(hash table)实现的map。而哈希表的组成是一个数组,而数组的元素是则单向链表的首节点。



2.特点

线程安全,并且不允许 key 或 value 为 null 。

与 HasMap 的底层结构相同,不同的是:

HashMap 允许 key,value 为 null;

HashMap 的初始容量必须为 2 的倍数,而 HashTable 只要求不为 0 即可;

关于数组索引位置的计算公式不同。

3.初始容量 和加载因子

Hashtable 的实例有两个参数影响其性能:初始容量 和加载因子

容量,是哈希表中桶(bucket)的数量,初始容量 就是哈希表创建时的容量。在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。

初始容量,主要控制空间消耗与执行 rehash 操作所需要的时间损耗之间的平衡。如果初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生 rehash 操作。但是,将初始容量设置太高可能会浪费空间。

加载因子,是对哈希表在其容量自动增加之前可以达到多满的一个尺度。

默认加载因子(.75),在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。

源码分析

1.节点

HashTable 中存放的元素,也称节点,由 Entry 构成。结构如图所示:



观察它的构造函数,是由 h(哈希值),key(键),value(值),Entry(下一个节点)这几个参数组成。

通过 key-value 构成了 map 的 映射关系

通过 Entry (即 next)连接它的下一节点,构成了单向链表

private static class Entry<K, V> implements Map.Entry<K, V> {
int hash;
K key;
V value;
Entry<K, V> next;

// 构造函数
protected Entry(int hash, K key, V value, Entry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

// 计算哈希码值
public int hashCode() {
return hash ^ (value == null ? 0 : value.hashCode());
}

//....省略剩余方法
}


2.构造函数

观察代码构造函数 ①~③ 都调用了构造函数 ④ 。

而该方法主要完成了参数的初始化(loadFactor,threshold)以及数组(table [])的创建工作。

注意:在 HashMap 中,初始化容量(initialCapacity) 必须是 2 的倍数,而 HashTable 只要求不为 0 即可。

// 内部数组
private transient Entry[] table;

// 加载因子
private float loadFactor;

// 临界值
private int threshold;

// 实际存放元素个数
private transient int count;

// 修改次数
private transient int modCount = 0;

// ①
public Hashtable() {
this(11, 0.75f);
}

// ②
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}

// ③
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2 * t.size(), 11), 0.75f);

// 添加 map 的所有映射(在添加操作会介绍)
putAll(t);
}

// ④ --> 负责具体实现
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);
}

// 在 HashMap 初始化容量必须为 2的倍数
if (initialCapacity == 0) {
initialCapacity = 1;
}

// 初始化参数,并创建数组
this.loadFactor = loadFactor;
table = new Entry[initialCapacity];
threshold = (int) (initialCapacity * loadFactor);
}


3.添加操作(putAll,put)

这里提供了两种了添加方式, putAll 会去遍历指定 map 的所有 key-value,然后再调用 put 将其逐个添加进哈希表。因此重点来看 put 的工作流程,如下图所示:



判断 value 是否为空,为空抛出。与 HashMap 不同, 在 HashTable 中不允许空值,而 HashMap 则允许

计算 key-value 在哈希表中的位置。同样,这里的计算公式也与 HashMap 有差别

找到该位置的节点(table 数组中存放的都是单链表的首节点),遍历操作。

比较 key,存在则替换 key;

不存在,先扩充容量。再添加 key-value 作用新的首节点。

//添加指定的 map
public synchronized void putAll(Map<? extends K, ? extends V> t) {
// 遍历 Map 的映射关系
for (Map.Entry<? extends K, ? extends V> e : t.entrySet()) {
// 添加一个映射关系
put(e.getKey(), e.getValue());
}
}

//添加单个映射关系
public synchronized V put(K key, V value) {

// 与 HashMap 不同,不允许 value 为空
if (value == null) {
throw new NullPointerException();
}

Entry tab[] = table;

// 与 HashMap 计算方式不同,计算得到数组的索引位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

// 遍历该位置的单链表
for (Entry<K, V> e = tab[index]; e != null; e = e.next) {

// 存在相同的 key,替换 value,并返回旧值
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}

// 若不存在相同的 key,扩充完容量,并重新计算索引值
modCount++;
if (count >= threshold) {
// 扩充容量
rehash();
tab = table;

// 重新计算索引位置
index = (hash & 0x7FFFFFFF) % tab.length;
}

// 添加新节点为首节点
Entry<K, V> e = tab[index];
tab[index] = new Entry<K, V>(hash, key, value, e);

count++;
return null;
}

// 调整 HashTable 的容量,=(旧容量*2+1)
protected void rehash() {
int oldCapacity = table.length;
Entry[] oldMap = table;
// 扩充容量
int newCapacity = oldCapacity * 2 + 1;
// 创建新数组
Entry[] newMap = new Entry[newCapacity];
modCount++;
threshold = (int) (newCapacity * loadFactor);
table = newMap;
// 复制元素到新数组
for (int i = oldCapacity; i-- > 0;) {
for (Entry<K, V> old = oldMap[i]; old != null;) {
Entry<K, V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
}
}
}


4.删除操作(remove,clear)

同样有两种删除操作。重点来看下 remove 。观察代码,发现与 put 的流程相似。不同的是 put 是添加/修改,而它是删除

这里来分析删除操作,即从单链表中移除节点的具体流程:

首先要判断是不是首节点

若是首节点的话,则将它的下一节点设为数组元素(因为在数组中存在的都是单链表的首节点)。并将该节点置空,等待 gc 回收。

若不是,则需要修改该节点前置节点的指针,将其指向该节点的下一节点。如下图所示:



// 移除指定的映射关系
public synchronized V remove(Object key) {

Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

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++;
// 判断该节点是不是首节点
if (prev != null) {
// 若不是,修改指针域
prev.next = e.next;
} else {
// 若是,将下一节点添加进数组设置为首节点
tab[index] = e.next;
}

count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}

// 清空操作
public synchronized void clear() {
Entry tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0;) {
tab[index] = null;
}
count = 0;
}


5.查询操作(get,containsKey,contains)

观察 getcontainsKey 的代码,发现二者的代码基本相同。甚至与 remove,put 也相差不大。

都遵循了[计算哈希值 -> 计算索引位置 -> 遍历单链表 -> 判断 key 是否相同 -> …] 这几个步骤。

因此这里重点来看下 contains 方法,它与 get,containsKey 不同。由于不能通过 value 得到数组的索引位置,只能遍历整个哈希表的元素(节点)。

因此它的步骤是 [判断是否为空 -> 遍历数组 -> 遍历单链表 -> 比较 value是否相同 -> …]

// 根据 key 找到对应的 value
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}

// 判断是否包含指定的 key
public synchronized boolean containsKey(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
return false;
}

// 判断是否包含指定的 value
public synchronized boolean contains(Object value) {
// 不允许 value 为空
if (value == null) {
throw new NullPointerException();
}
Entry tab[] = table;

// 从后向前遍历数组(因为不能从 value 计算中节点在哈希表中的位置 )
for (int i = tab.length; i-- > 0;) {
for (Entry<K, V> e = tab[i]; e != null; e = e.next) {
// 与 key 不同,只比较值
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}


6.遍历操作(entrySet,keySet,values)

分别实现了对 Entry(节点),key,value 的遍历操作。

以 entrySet 为例,该方法通过 Collections 的同步方法创建了一个 Hashtable.EntrySet(内部类) 的实例;

相应的 keySet ,values 分别创建了 Hashtable.KeySet,Hashtable.AbstractCollection 的实例。

private transient volatile Set<Map.Entry<K, V>> entrySet = null;

public Set<Map.Entry<K, V>> entrySet() {
if (entrySet == null) {
entrySet = Collections.synchronizedSet(new EntrySet(), this);
}
return entrySet;
}


以 Hashtable.EntrySet 为例子,重点来看它的 iterator 方法。

private static final int KEYS = 0;
private static final int VALUES = 1;
private static final int ENTRIES = 2;

private class EntrySet extends AbstractSet<Map.Entry<K, V>> {

// 迭代器
public Iterator<Map.Entry<K, V>> iterator() {
return getIterator(ENTRIES);
}

//...省略部分代码
}


在三个内部类的 iterator 方法中都调用 getIterator 方法来创建迭代器,通过传入的 type 区分类型。

在 getIterator 中,首先会判断哈希表的元素个数。若为 0,返回 HshTable.EmptyIterator;不为 0,返回 HshTable.Enumerator

private static Iterator emptyIterator = new EmptyIterator();

private <T> Iterator<T> getIterator(int type) {
if (count == 0) {
return (Iterator<T>) emptyIterator;
} else {
return new Enumerator<T>(type, true);
}
}


下面来分析下 Enumerator 的源码,当它表示迭代器时可以操作 next,hasNext ,remove方法;当它表示枚举类时,不能操作 remove 方法。

private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
Entry[] table = Hashtable.this.table;
int index = table.length;
Entry<K, V> entry = null;
Entry<K, V> lastReturned = null;
int type;

// Enumerator是 “迭代器(Iterator)” 还是 “枚举类(Enumeration)”的标志
// 为true,表示它是迭代器;否则,是枚举类
boolean iterator;

// 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制。
protected int expectedModCount = modCount;

// 构造函数
Enumerator(int type, boolean iterator) {
this.type = type;
this.iterator = iterator;
}

// 关键--> 判断迭代器是否还有数据
public boolean hasNext() {
return hasMoreElements();
}

public boolean hasMoreElements() {
Entry<K, V> e = entry;
int i = index;
Entry[] t = table;

// 从后向前遍历数组元素
while (e == null && i > 0) {
e = t[--i];
}
entry = e;
index = i;
return e != null;
}

// 关键 --> 取得下一个节点
public T next() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
return nextElement();
}

public T nextElement() {
Entry<K, V> et = entry;
int i = index;
Entry[] t = table;
/* Use locals for faster loop iteration */
while (et == null && i > 0) {
et = t[--i];
}
entry = et;
index = i;
if (et != null) {
Entry<K, V> e = lastReturned = entry;
entry = e.next;
return type == KEYS ? (T) e.key : (type == VALUES ? (T) e.value : (T) e);
}
throw new NoSuchElementException("Hashtable Enumerator");
}

// 移除当前迭代的节点
public void remove() {
// 当该类表示迭代器时才能调用该方法
if (!iterator) {
throw new UnsupportedOperationException();
}

if (lastReturned == null) {
throw new IllegalStateException("Hashtable Enumerator");
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}

synchronized (Hashtable.this) {
Entry[] tab = Hashtable.this.table;
int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;

// 计算索引位置,遍历单链表
for (Entry<K, V> e = tab[index], 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();
}
}
}


7.工具类方法

public synchronized int size() {
return count;
}

public synchronized boolean isEmpty() {
return count == 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java HashTable 集合