JAVA学习-Hashtable详解

1.定义

Hashtable的定义为:首先它是类似与HashMap的key-value的哈希表,不允许key-value为NULL值,另外一点值得注意的是Hashtable是线程安全的,根据Hashtable的这里将提出几个问题:

1.Hashtable的内部实现是怎样的?

2.Hashtable是如何实现线程安全的?

3.同样是key-value的集合,Hashtable与HashMap的区别是什么?

下文将围绕以上三个问题进行,另外本文源码来自与JDK_1.8.0_131

2.结构

2.1 类图结构

Hashtable类图

如上图所示,为Hashtable的类图结构,其主要实现、继承的接口、类如下:

  • 1.Dictionary 类: 该抽象类定义了key-value的集合,定义每个key与value都是一个对象,不允许null值作为key与value,值得注意的是这个类已经过时了,新的实现都需要实现Map接口而不是该抽象类
  • 2.Map 接口: 定义将键值映射到值的对象,Map规定不能包含重复的键值,每个键最多可以映射一个值,这个接口是用来替换Dictionary类.
  • 3.Cloneable 接口: 实现了该接口的类可以显示的调用Object.clone()方法,合法的对该类实例进行字段复制,如果没有实现Cloneable接口的实例上调用Obejct.clone()方法,会抛出CloneNotSupportException异常。正常情况下,实现了Cloneable接口的类会以公共方法重写Object.clone()
  • 4.Serializable 接口: 实现了该接口标示了类可以被序列化和反序列化,具体的 查询序列化详解

2.2 基本属性及构造方法

2.2.1 基本属性

如下源码所示,Hashtable中基本的属性如下,值得注意的是首先Hashtable中实际上记录一个数组,即Entry<?,?>[] table,这便是Hashtable的内部结构,再就是加载因子loadFactor这个属性,这个属性和初始容量影响了Hashtable的性能,loadFactor的默认值为0.75,这个是从时间和空间的角度综合考虑后设定的,在没有特殊的情况下建议不用修改这个值。

//Hashtable内部数组结构
private transient Entry<?,?>[] table;
//Hashtable元素总个数
private transient int count;
//扩容操作的阈值
private int threshold;
//加载因子 一般情况下为0.75
private float loadFactor;
//记录当前修改信息
private transient int modCount = 0;
2.2.2 构造方法

Hashtable有4个构造方法,如下所示:

  • public Hashtable(int initialCapacity, float loadFactor):参数指定初始化容量和加载因子
  • public Hashtable(int initialCapacity):参数指定初始化容量,其内部实际上是调用了上一个构造方法,其加载因子是默认的值 0.75
  • public Hashtable():默认的构造方法,其内部调用了第一个构造方法,指定的初始化容量为11,加载因子是 0.75
  • public Hashtable(Map<? extends K, ? extends V> t):将一个Map类型的集合全部添加到Hashtable中,内部实际调用了putAll方法

3.原理

3.1 内部结构

Hashtable内部结构

上图为Hashtable的内部结构,实际上我们通过Entry<?,?>[] table就可以看出,Hashtable内部为一个Entry<?,?>类型的数组,而Entry的结构如下所示,从而可以看出Hashtable是数组连表,既然是一个数组链表就会存在hash冲突的情况,下面就通过Hashtable中的实现细节,来探寻其中的奥秘。

private static class Entry<K,V> implements Map.Entry<K,V> {
    //hash值
    final int hash;
    //对应元素的key值
    final K key;
    //对应元素的value值
    V value;
    //指向下一个节点的引用
    Entry<K,V> next;
    //...省略部分代码
 }

3.2 实现细节

3.2.1 put方法
 public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    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;
}

如上为Hashtable中添加key-value的方法,通过源码可以看出主要流程分为以下几步:

  • 1.判空处理:对于value为空的情况,将抛出NullPointerException,在定义时发现Hashtable是不允许key,value都为null的,但这里为什么没有加以判断呢,原来每个key值将会获取其hash,即必须调用key.hashCode()方法,此时key为null是也会抛出NullPointerException,这也就是为什么Hashtable不允许key,value为NULL值。
  • 2.定位:这一步其实很好理解,由于Hashtable是数组链表结构,首先需要定位到其在数组中的位置,使用(hash & 0x7FFFFFFF) % tab.length的方式,有可能你会奇怪 hash & 0x7FFFFFFF 这个有什么作用,我的理解是因为hash值是int类型,那么hash值有可能是负数,而负数的二进制标志是最高位,则和0x7FFFFFFF做与操作即是将负数变成正数,确保了获取到的index是正数。
  • 3.遍历:遍历主要是查看是否已经存在需要添加的key-value,若已经存在则用新值替换老值,并返回老值,否则新增节点,这个操作主要是在addEntry方法中进行,如下是addEntry方法的源码,其流程是判断当前元素个数是否大于扩容阈值,若大于则rehash,否则新增节点并将该节点添加到对应的位置
private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    //判断当前元素个数是否大于扩容阈值,若大于则rehash,否则
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}
3.2.2 rehash方法

在上文中提到在当前元素个数大于扩容阈值时,会调用rehash方法进行扩容操作并且重新分布元素的位置,而阈值threshold=capacity * loadFactor,所以当capacity一定时,可以通过负载因子loadFactor去控制阈值的大小,负载因子loadFactor越大则阈值threshold越大,反而
负载因子loadFactor越小则阈值threshold越小,可以根据实际情况调整负载因子的大小从而调节Hashtable的性能。

下面为rehash方法的源码

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //扩容为源码的2*oldCapacity + 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;
    }
    //新建扩容后的数组
    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的过程实际上是扩容并重新分布的过程,主要包括以下几个步骤:

  • 1.扩容:需要注意的是扩容是2*原容量 + 1
  • 2.创建新数组:创建一个新的Entry<?,?>[],其容量为扩容后的新的容量
  • 3.分布元素:将旧数组中的元素分布到新数组中
3.2.3 get方法

如下为Hashtable通过key值获取对应的value值的方法,其流程比较简单,和添加中存在部分类似,根据key值定位(此时若key为null,也将会报NullPointerException),然后遍历查找对应的值,若没找到则返回null

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;
}
3.2.3 常用方法

如下列表中是Hashtable中常用的方法及其说明

方法 说明
public synchronized boolean contains(Object value) 测试此映射表中是否存在与指定值关联的键。
public synchronized V get(Object key) 返回指定键所映射到的值
public synchronized V put(K key, V value) 将指定 key 映射到此哈希表中的指定 value。
public synchronized V remove(Object key) 从哈希表中移除该键及其相应的值。
public synchronized void clear() 将此哈希表清空,使其不包含任何键。
public boolean containsValue(Object value) 判断Hashtable是否包含某个值
public synchronized boolean containsKey(Object key) 判断Hashtable是否包含指定的key

4.对比

JAVA学习-HashMap详解一文中详细介绍了HashMap的实现细节,其实通过本文的介绍你会发现Hashtable与HashMap存在很多相似的地方,下面来介绍下HashMap与Hashtable的区别:

  • 1.实现:HashMap继承的类是AbstractMap类,而Hashtable继承的是Dictionary类,而Dictionary是一个过时的类,因此通常情况下建议使用HashMap而不是使用Hashtable
  • 2.内部结构:其实HashMap与Hashtable内部基本都是使用数组-链表的结构,但是HashMap引入了红黑树的实现,内部相对来说更加复杂而性能相对来说应该更好
  • 3.NULL值控制:通过前面的介绍我们知道Hashtable是不允许key-value为null值的,Hashtable对于key-value为空的情况下将抛出NullPointerException,而HashMap则是允许key-value为null的,HashMap会将key=null方法index=0的位置。
  • 4.线程安全:通过阅读源码可以发现Hashtable的方法中基本上都是有synchronized关键字修饰的,但是HashMap是线程不安全的,故对于单线程的情况下来说HashMap的性能更优于Hashtable,单线程场景下建议使用HashMap.

总的来说,建议在单线程的情况下尽量使用HashMap。

5.总结

本文主要介绍了Hashtable内部实现,通过源码了解其内部的流程,最后也也介绍了Hashtable与HashMap的区别,若有问题,望指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容