07.Java 集合 - HashTable
来源:互联网 发布:ubuntu下安装eclipse 编辑:程序博客网 时间:2024/06/06 14:04
基本概念
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)
观察 get 与 containsKey 的代码,发现二者的代码基本相同。甚至与 remove,put 也相差不大。
都遵循了[计算哈希值 -> 计算索引位置 -> 遍历单链表 -> 判断 key 是否相同 -> …] 这几个步骤。
因此这里重点来看下 contains 方法,它与 get,containsKey 不同。由于不能通过 value 得到数组的索引位置,只能遍历整个哈希表的元素(节点)。
因此它的步骤是 [判断是否为空 -> 遍历数组 -> 遍历单链表 -> 比较 value是否相同 -> …]
// 根据 key 找到对应的 valuepublic 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;}// 判断是否包含指定的 keypublic 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;}// 判断是否包含指定的 valuepublic 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;}
- 07.Java 集合 - HashTable
- java集合TreeMap、Hashtable
- Java集合之Hashtable
- java集合------HashTable
- JAVA集合容器---Hashtable
- java集合-HashTable
- Java集合之Hashtable
- java--集合--HashTable
- Java集合---HashTable原理分析
- Java集合Hashtable源码解析
- 【Java集合】HashMap和Hashtable
- 【Java集合类】HashTable解析
- 类似Java Hashtable的Js集合类
- java集合类HashTable、HashMap比较
- java基础 数据集合 hashmap hashtable
- 【Java集合源码剖析】Hashtable源码剖析
- 【Java集合源码剖析】Hashtable源码剖析
- Java Map集合之HashMap、HashTable
- win7 VMware SVGA 3D驱动
- 使用sqlalchemy 辅助统计学生期末成绩
- LeetCode Nim Game
- 【Android】PropertyAnimation属性动画
- c语言题目
- 07.Java 集合 - HashTable
- mac上运行hadoop2.x报unable to load native-hadoop library错误
- Oracle学习04【持续更新】
- c# API
- Excel宏教程 (宏的介绍与基本使用)
- C++ Build MessageBox将按钮的文字显示为英文
- jQuery——jquery.fn.extend与jquery.extend
- java死锁代码示例
- 一道计算一元二次方程解的算法题