2.Hashtable

Hashtable,顾名思义,哈希表,本来是已经被淘汰的内容,但在某一版本的Java将其实现了Map接口,因此也成为常用的集合类,但是hashtable由于和hashmap十分相似,因此据说也成为“面试经典题”。由于两者的区别网上实在太多太多,我就不自己在摸索了直接拷贝过来用于借鉴:

  • HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行。
  • HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 提供了ConcurrentHashMap,它解决了HashMap的线程不安全的问题和Hashtable效率低的问题。
  • 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
  • 还有一点,算不上区别吧,HashMap是继承实现了Map接口的虚类AbstractMap,而Hashtable继承Dictionary类,并且实现Map接口。

至于前三点可以说是最多被提及的,当然HashMap可以通过下面的语句进行同步:

Map m = Collections.synchronizeMap(hashMap);

然后我们来深入讨论一下其他的的几个区别

  • 是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不仅仅是fail-fast的,还使用了Enumeration的方式。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

前面咱们已经讨论过HashMap的一些遍历方式,可以确定的是HashMap的遍历方式Hashtable都有,但是Hashtable在此基础上还有Enumeration的方式,我们直接以实例来展示:

    public static void main(String [] args)
{
Hashtable ht = new Hashtable<String, Integer>();
for(int i=100;i<105;i++)
{
ht.put(String.valueOf(i)+"th",i);
}
System.out.println(ht);
Enumeration enu = ht.keys();
while(enu.hasMoreElements()) {
System.out.println(enu.nextElement());
} }

结果如下:

{102th=102, 104th=104, 101th=101, 103th=103, 100th=100}
102th
104th
101th
103th
100th

也证明Hashtable或者HashMap不是所谓的有序排列。keys()遍历Hashtable的键,同样的elements()也会遍历Hashtable的值。

为了内容的延续性我们放到后面来讲fail-fast机制。接下来还是看两个集合的区别。

  • 扩容机制不同

在此之前,先看一下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);
} public Hashtable() {
this(11, 0.75f);
} public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}

Hashtable Construction Functions

可以看到Hashtable的默认capacity是11,而不是HashMap的16。而当参数是Map时,选择11与Map2倍的size较大的一个。而扩容的方法在rehash()方法中:

    protected void rehash() {
int oldCapacity = table.length;
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; 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;
}
}
}

rehash function

仔细分块来看:

        int oldCapacity = table.length;
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;
}

前两句是为了存储旧集合及其length(不是某个类的域,请回忆起Java最初求数组长度的方法)。接着新容量(newCapacity)等于旧容量*2+1,判断新容量是否超过最大值,如果超过的情况下判断旧容量是否等于最大值,如果旧容量已经等于最大值,那说明容量已经扩无可扩,直接返回。否则就扩到所允许的最大值。

一直使用最大值来替代常量MAX_ARRAY_SIZE是为了方便,设置这个值是为了防止out of memory,它等于Integer.MAX_VALUE - 8;

        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;

Hashtable采用了直接申请一个新的数组的方法,重新计算阈值,newCapacity不是小于MAX_ARRAY_SIZE吗?为什么还要从这里面选一个较小的?事实上,loadFactor不一定小于1,可以设置大于1,因此就会出现前者大于后者的情况。

        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;
}
}

这段代码将旧的数据结构转移到新的数据结构中,首先从后向前的遍历链表数组,对于其第i个元素(一个链表)来说,将其赋值给old,然后开始遍历old,对于old的每一个节点计算出它在新的下标值,然后插入。注意插入的两句代码,e.next = (Entry<K,V>)newMap[index]; 是指将新哈希表中这个位置的元素头节点置于e(等价于old)之后;而newMap[index] = e; 代表将e称为这个下标位置的新节点。

计算下标的公式是(e.hash & 0x7FFFFFFF) % newCapacity 表示计算出该节点的哈希值与0x7FFFFFFF相位与,而这个十六进制的数转换为二进制就是31个1,而hash值应该是32位的,因此这部操作的目的就是保持哈希值为正,即首位为0.

在HashMap中的扩容方法是resize()方法:

    final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

resize function

        Node<K,V>[] oldTab = table;                          //记录旧表
int oldCap = (oldTab == null) ? 0 : oldTab.length; //计算旧的capacity
int oldThr = threshold; //记录旧阈值
int newCap, newThr = 0; //声明新capacity和threshold
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}

除了一些声明之外,这段代码主要处理当旧容量不是0的时候,说明已经被初始化过,只需进行扩容操作即可,如果旧容量大于等于MAXIMUM_CAPACITY(为什么会大于这个值呢?),那么指提升阈值就好,因为容量已经无法提升,而提升阈值可以一定程度的满足不扩容的条件。否则,新容量等于旧容量的2倍,若新容量小于MAXIMUM_CAPACITY且旧容量大于DEFAULT_INITIAL_CAPACITY,阈值也相应的翻倍。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;

上述是两个常量的值,至于为何要求旧容量要小于DEFAULT_INITIAL_CAPACITY才更新阈值,博主也不清楚,希望有大佬帮忙回答。

        else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}

当旧阈值大于零且旧容量等于0时(因为容量不可能小于0),就将旧阈值赋予新容量,否则使用默认值,一开始真的不知道什么情况会触发这个条件,即旧容量和旧阈值都是0,我测试了构造函数容量设置为0,此时通过tablesizefor计算出的初始化阈值为1。这时我就更晕了,当我把构造函数的的容量设置为1的时候,很奇怪的就会出现的那种情况。这是我一步步的debug出来的,但是并不理解为什么参数是1容量却初始化为0。 当前面步骤执行完毕后,判断新阈值是否为0,是则执行newCap*loadFactor,之后再去除大于MAXIMUM_CAPACITY的情况,并且取整。

        threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) { //旧集合不为空
for (int j = 0; j < oldCap; ++j) { //遍历旧集合
Node<K,V> e;
if ((e = oldTab[j]) != null) { //某桶不为空
oldTab[j] = null;
if (e.next == null) //该桶的链表只有一个元素
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) //不只有一个元素而且已经转成了红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order //不知有一个元素但是仍是链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;

这一段看起来比较长,需要耐心地看,大致地了解这一段代码的作用就是将原数据结构中的数据迁移到新的数据结构。上述的注释基本上能够理清逻辑,剩下就是链表的迁移,在此之前需要介绍一下一些里面用到的位运算的知识:

(e.hash & oldCap)这个运算其实是判断是否需要移动位置。
如果不需要移动,若新链表为空,那么直接把e设置为头节点,不为空就把e放在新链表的尾部;最后把尾节点设置为e。
如果需要移动,按照同样的步骤存储在另一个链表上。而移动的下标由j转变为oldCap+j,无需重新计算哈希。详细如图所示:


总体来说HashMap1.7查找时间复杂度从O(1)到O(N)不等,如果所有元素都映射到同一个桶中,那么哈希图退化称为链表,此时是最坏的情况O(N);1.8中红黑树查找复杂度为O(logn),性能上有一定程度的提升。

从上面分析扩容代码来看,我们仍然能找出新的不同:

  • HashMap有红黑树的参与,而Hashtable没有
  • Hashtable多一个contains()方法,等同于containsValue()方法。
  • hash值的计算方法不同

先看一下HashMap的计算方式:

 插入时计算下标位置:
if ((p = tab[i = (n - 1) & hash]) == null) hash值的来源:
hash(key) hash()方法的详情:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode()方法是Object类中的自带方法。
插入计算下标位置:            index = (hash & 0x7FFFFFFF) % tab.length;

hash值的计算方法:            hash = key.hashCode();

3. fail-fast机制

什么是fail-fast,它是java集合中一种错误机制,例如当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

解决方法,换用其他的集合,例如ArrayList换为CopyOnWriteArrayList等。ConcurrentHashMap替代HashMap,简单粗暴。

集合(四) Hashtable的更多相关文章

  1. C#基础课程之五集合(HashTable,Dictionary)

    HashTable例子: #region HashTable #region Add Hashtable hashTable = new Hashtable(); Hashtable hashTabl ...

  2. Java之集合(十四)Hashtable

    转载请注明源出处:http://www.cnblogs.com/lighten/p/7426522.html 1.前言 HashTable这个类很奇特,其继承了Dictionary这个没有任何具体实现 ...

  3. Map Hashtable Hashmap 集合四

    Map是通过键值对来唯一标识的,所以不能重复 存相同键值对 Hashtable存的是键值对 Hashtable<key,value> key,value 都不能为null 方法get(); ...

  4. C#集合之Hashtable

    Hashtable是一个键值对集合,其泛型版本是Dictionary<K, V>,下面说下常用的一些方法; 1.Add(),向Hashtable添加元素,需要注意的是因为它是键值对集合,所 ...

  5. 集合之HashTable

    在java中与有两个类都提供了一个多种用途的hashTable机制,他们都可以将可以key和value结合起来构成键值对通过put(key,value)方法保存起来,然后通过get(key)方法获取相 ...

  6. 从内部剖析C#集合之HashTable

    计划写几篇文章专门介绍HashTable,Dictionary,HashSet,SortedList,List 等集合对象,从内部剖析原理,以便在实际应用中有针对性的选择使用. 这篇文章先介绍Hash ...

  7. 从内部剖析C# 集合之---- HashTable

    这是我在博客园的第一篇文章,写的不好或有错误的地方,望各位大牛指出,不甚感激. 计划写几篇文章专门介绍HashTable,Dictionary,HashSet,SortedList,List 等集合对 ...

  8. Stack集合、queue集合、hashtable集合

    1.栈:Stack,先进后出,一个一个赋值,一个一个取值,按顺序. .count           取集合内元素的个数 .push()         将元素一个一个推入集合中//stack集合存入 ...

  9. Java集合之Hashtable

    和HashMap一样,Hashtable也是一个散列表,存储的内容也是键值对key-value映射.它继承了Dictionary,并实现了Map.Cloneable.io.Serializable接口 ...

随机推荐

  1. 使用Keepalived实现Nginx高可用

    Keepalived是一个路由软件,可以提供linux系统和linux系统上的组件的负载均衡和高可用,高可用基于VRRP(Virtual Router Redundancy Protocol,虚ip) ...

  2. vlc 控件属性和方法

    VLC调研 VLC控件支持的参数和方法 VLC对象列表 Vlc Plugin Object的方法 l        VersionInfo:成员, 返回版本信息的字符串 l        vlc.ve ...

  3. Spark分区实例(teacher)

    package URL1 import org.apache.spark.Partitioner import scala.collection.mutable class MyPartitioner ...

  4. Eclipse 4.11 Debug jar包代码时进入空心J

    代码调试时,进入jar包中的时候,会出现如下的情况超级影响代码调试 断点打在上面的地方,但是却进入到了空心J的那个地方了. 解决办法:去掉勾选即可. 我是这么解决的.

  5. pthon基础知识(索引、切片、序列相加、乘法、检查元素是否是序列成员、计算序列长度、最大最小值)

    序列   数据存储方式  数据结构 python 列表.元组.字典.集合.字符串 序列: 一块用于存放多个值的连续内存空间,并且按一定顺序排列,可以通过索引取值 索引(编号): 索引可以是负数 从左到 ...

  6. 学习shell的第一天

    1.命令历史 作用:查之前使用的命令  关于命令历史的文件  每个用户家目录下面的 .bash_history  在关机的时候,会自动写入一次 (history -a  将内存中的命令历史写入文件)  ...

  7. 针对yarn的8088端口攻击

    参考: https://www.wangbokun.com/%E8%BF%90%E7%BB%B4/2019/09/02/%E6%8C%96%E7%9F%BF%E7%97%85%E6%AF%92.htm ...

  8. 通过java 来实现对多个文件的内容合并到一个文件中

    现在有多个txt文本文件,需要把这么多个文件的内容都放到一个文件中去 以下是实现代码 package com.SBgong.test; import java.io.*; public class F ...

  9. selenium爬虫后上传数据库。

    一.准备工作 1.1安装软件 安装python.安装谷歌浏览器.将chromedriver.exe放到指定位置.放到Scripts文件夹中.我这边的路径为:C:\Users\1\AppData\Loc ...

  10. ARM Cortex-M 系列 MCU 错误追踪库 心得

    一. 感谢CmBacktrace开源项目,git项目网站:https://github.com/armink/CmBacktrace 二. 移植CmBacktrace 2.1 准备好CmBacktra ...