深入浅出的分析 Hashtable

作者:炸鸡可乐
原文出处:www.pzblog.cn

一、摘要

在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties 等等。

深入浅出的分析 Hashtable

本文主要从数据结构和算法层面,探讨 Hashtable 的实现,如果有理解不当之处,欢迎指正。

二、简介

Hashtable 一个元老级的集合类,早在 JDK 1.0 就诞生了,而 HashMap 诞生于 JDK 1.2,在实现上,HashMap 吸收了很多 Hashtable 的思想,虽然二者的底层数据结构都是 数组 + 链表 结构,具有查询、插入、删除快的特点,但是二者又有很多的不同。

打开 Hashtable 的源码可以看到,Hashtable 继承自 Dictionary,而 HashMap 继承自 AbstractMap。

public class Hashtable
    extends Dictionary
    implements Map, Cloneable, java.io.Serializable {
    .....

}

HashMap 继承自 AbstractMap,HashMap 类的定义如下:

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
    .....

}

其中 Dictionary 类是一个已经被废弃的类,翻译过来的意思是 这个类已经过时,新的实现应该实现 Map 接口而不是扩展此类,这一点我们可以从它代码的注释中可以看到:

/**
 * NOTE: This class is obsolete.  New implementations should
 * implement the Map interface, rather than extending this class.

 */
public abstract
class Dictionary {
    ......

}

Hashtable 和 HashMap 的底层是以数组来存储,同时,在存储数据通过 key计算数组下标的时候,是以哈希算法为主,因此可能会产生哈希冲突的可能性。

通俗的说呢,就是不同的 key,在计算的时候,可能会产生相同的数组下标,这个时候,如何将两个对象放入一个数组中呢?

而解决哈希冲突的办法,有两种,一种开放地址方式(当发生 hash 冲突时,就继续以此继续寻找,直到找到没有冲突的hash值),另一种是拉链方式(将冲突的元素放入链表)。

Java Hashtable 采用的就是第二种方式,拉链法!

于是,当发生不同的 key通过一系列的哈希算法计算获取到 相同的数组下标的时候,会将对象放入一个数组容器中,然后将对象以 单向链表的形式存储在同一个数组下标容器中,就像链子一样,挂在某个节点上,如下图:

深入浅出的分析 Hashtable

与 HashMap 类似,Hashtable 也包括五个成员变量:

/**由Entry对象组成的数组*/
private transient Entry[] table;

/**Hashtable中Entry对象的个数*/
private transient int count;

/**Hashtable进行扩容的阈值*/
private int threshold;

/**负载因子,默认0.75*/
private float loadFactor;

/**记录修改的次数*/
private transient int modCount = 0;

具体各个变量含义如下:

  • table:表示一个由 Entry 对象组成的链表数组,Entry 是一个单向链表,哈希表的 key-value键值对都是存储在 Entry 数组中的;
  • count:表示 Hashtable 的大小,用于记录保存的键值对的数量;
  • threshold:表示 Hashtable 的阈值,用于判断是否需要调整 Hashtable 的容量,threshold 等于 容量 * 加载因子;
  • loadFactor:表示负载因子,默认为 0.75;
  • modCount:表示记录 Hashtable 修改的次数,用来实现快速失败抛异常处理;

接着来看看 Entry这个内部类, Entry用于存储链表数据,实现了 Map.Entry接口,本质是就是一个映射(键值对),源码如下:

 private static class Entry implements Map.Entry {
         /**hash值*/
        final int hash;
        /**key表示键*/
        final K key;
        /**value表示值*/
        V value;
        /**节点下一个元素*/
        Entry next;
        ......

}

我们再接着来看看 Hashtable 初始化过程,核心源码如下:

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

this 调用了自己的构造方法,核心源码如下:

public Hashtable(int initialCapacity, float loadFactor) {
        .....

        //默认的初始大小为 11
        //并且计算扩容的阈值
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

可以看到 HashTable 默认的初始大小为 11,如果在初始化给定容量大小,那么 HashTable 会直接使用你给定的大小

扩容的阈值 threshold等于 initialCapacity * loadFactor,我们在来看看 HashTable 扩容,方法如下:

protected void rehash() {
        int oldCapacity = table.length;
        //将旧数组长度进行位运算,然后 +1
        //等同于每次扩容为原来的 2n+1
        int newCapacity = (oldCapacity << 1) + 1;

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

        Entry[] newMap = new Entry[newCapacity];
}

可以看到, HashTable 每次扩充为原来的 2n+1

我们再来看看 HashMap,如果是执行默认构造方法,会在扩容那一步,进行初始化大小,核心源码如下:

final Node[] resize() {
    int newCap = 0;

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

    newCap = DEFAULT_INITIAL_CAPACITY;//默认容量为 16
    Node[] newTab = (Node[])new Node[newCap];
}

可以看出 HashMap 的默认初始化大小为 16,我们再来看看,HashMap 扩容方法,核心源码如下:

final Node[] resize() {
    //获取旧数组的长度
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int newCap = 0;

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

    //当进行扩容的时候,容量为 2 的倍数
    newCap = oldCap << 1;
    Node[] newTab = (Node[])new Node[newCap];
}

可以看出 HashMap 的扩容后的数组数量为原来的 2 倍

也就是说 HashTable 会尽量使用素数、奇数来做数组的容量,而 HashMap 则总是使用 2 的幂作为数组的容量。

我们知道当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,所以单从这一点上看,HashTable 的哈希表大小选择,似乎更高明些。

Hashtable 的 hash 算法,核心代码如下:

//直接计算key.hashCode()
int hash = key.hashCode();

//通过除法取余计算数组存放下标
// 0x7FFFFFFF 是最大的 int 型数的二进制表示
int index = (hash & 0x7FFFFFFF) % tab.length;

从源码部分可以看出,HashTable 的 key 不能为空,否则报空指针错误!

但另一方面我们又知道,在取模计算时,如果模数是 2 的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。 所以在 hash 计算数组下标的效率上,HashMap 却更胜一筹,但是这也会引入了哈希分布不均匀的问题, HashMap 为解决这问题,又对 hash 算法做了一些改动,具体我们来看看。

HashMap 的 hash 算法,核心代码如下:

/**获取hash值方法*/
static final int hash(Object key) {
     int h;
     // h = key.hashCode() 为第一步 取hashCode值(jdk1.7)
     // h ^ (h >>> 16)  为第二步 高位参与运算(jdk1.7)
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//jdk1.8
}

/**获取数组下标方法*/
static int indexFor(int h, int length) {
    //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

HashMap 由于使用了2的幂次方,所以在取模运算时不需要做除法,只需要位的与运算就可以了。但是由于引入的 hash 冲突加剧问题,HashMap 在调用了对象的 hashCode 方法之后,又做了一些高位运算,也就是第二步方法,来打散数据,让哈希的结果更加均匀。

与此同时,在 jdk1.8 中 HashMap 还引进来红黑树实现,当冲突链表长度大于 8 的时候,会将链表结构改变成红黑树结构,让查询变得更快, 具体实现可以参见《集合系列》中的 HashMap 分析

三、常用方法介绍

3.1、put方法

put 方法是将指定的 key, value 对添加到 map 里。

put 流程图如下:

深入浅出的分析 Hashtable

打开 HashTable 的 put 方法,源码如下:

public synchronized V put(K key, V value) {
        //当 value 值为空的时候,抛异常!
        if (value == null) {
            throw new NullPointerException();
        }

        Entry tab[] = table;

        //通过key 计算存储下标
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;

        //循环遍历数组链表
        //如果有相同的key并且hash相同,进行覆盖处理
        Entry entry = (Entry)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;
}

put 方法中的 addEntry 方法,源码如下:

private void addEntry(int hash, K key, V value, int index) {
        //新增修改次数
        modCount++;

        Entry tab[] = table;
        if (count >= threshold) {
           //数组容量大于扩容阀值,进行扩容
            rehash();

            tab = table;
            //重新计算对象存储下标
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        //将对象存储在数组中
        Entry e = (Entry) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
}

addEntry 方法中的 rehash 方法,源码如下:

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

        //每次扩容为原来的 2n+1
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                //大于最大阀值,不再扩容
                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 old = (Entry)oldMap[i] ; old != null ; ) {
                Entry e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry)newMap[index];
                newMap[index] = e;
            }
        }
}

总结流程如下:

  • 1、通过 key 计算对象存储在数组中的下标;
  • 2、如果链表中有 key,直接进行新旧值覆盖处理;
  • *3、如果链表中没有 key,判断是否需要扩容,如果需要扩容,先扩容,再插入数据;

有一个值得注意的地方是 put 方法加了 synchronized 关键字,所以,在同步操作的时候,是线程安全的。

3.2、get方法

get 方法根据指定的 key 值返回对应的 value。

get 流程图如下:

深入浅出的分析 Hashtable

打开 HashTable 的 get 方法,源码如下:

public synchronized V get(Object key) {
        Entry tab[] = table;
        //通过key计算节点存储下标
        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;
}

同样,有一个值得注意的地方是 get 方法加了 synchronized 关键字,所以,在同步操作的时候,是线程安全的。

3.3、remove方法

remove 的作用是通过 key 删除对应的元素。

remove 流程图如下:

深入浅出的分析 Hashtable

打开 HashTable 的 remove 方法,源码如下:

public synchronized V remove(Object key) {
        Entry tab[] = table;
        //通过key计算节点存储下标
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        Entry e = (Entry)tab[index];
        //循环遍历链表,通过hash和key判断键是否存在
        //如果存在,直接将改节点设置为空,并从链表上移除
        for(Entry 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;
}

同样,有一个值得注意的地方是 remove 方法加了 synchronized 关键字,所以,在同步操作的时候,是线程安全的。

四、总结

总结一下 Hashtable 与 HashMap 的联系与区别,内容如下:

  • 1、虽然 HashMap 和 Hashtable 都实现了 Map 接口,但 Hashtable 继承于 Dictionary 类,而 HashMap 是继承于 AbstractMap;
  • 2、HashMap 可以允许存在一个为 null 的 key 和任意个为 null 的 value,但是 HashTable 中的 key 和 value 都不允许为 null;
  • 3、Hashtable 的方法是同步的,因为在方法上加了 synchronized 同步锁,而 HashMap 是非线程安全的;

尽管,Hashtable 虽然是线程安全的,但是我们一般不推荐使用它,因为有比它更高效、更好的选择 ConcurrentHashMap,在后面我们也会讲到它。

最后,引入来自 HashTable 的注释描述:

If a thread-safe implementation is not needed, it is recommended to use HashMap in place of Hashtable. If a thread-safe highly-concurrent implementation is desired, then it is recommended to use java.util.concurrent.ConcurrentHashMap in place of Hashtable.

简单来说就是,如果你不需要线程安全,那么使用 HashMap,如果需要线程安全,那么使用 ConcurrentHashMap。

HashTable 已经被淘汰了,不要在新的代码中再使用它。

五、参考

1、JDK1.7&JDK1.8 源码

2、博客园 – 程序员赵鑫 – HashMap和HashTable到底哪不同?

Original: https://www.cnblogs.com/dxflqm/p/11947384.html
Author: 程序员志哥
Title: 深入浅出的分析 Hashtable

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/612908/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 通过Python收集MySQL MHA 部署及运行状态信息的功能实现

    一. 背景介绍 当集团的MySQL数据库实例数达到2000+、MHA集群规模数百个时,对MHA的及时、高效管理是DBA必须面对的一个挑战。MHA 集群 节点信息 和 运行状态 是管…

    数据库 2023年6月16日
    0118
  • JUC学习笔记(四)

    JUC学习笔记(一)https://www.cnblogs.com/lm66/p/15118407.htmlJUC学习笔记(二)https://www.cnblogs.com/lm…

    数据库 2023年6月6日
    0105
  • 第17章 触发器

    第17章 触发器 在实际开发中,我们经常会遇到这样的情况:有 2 个或者多个相互关联的表,如 &#x5546;&#x54C1;&#x4FE1;&#x…

    数据库 2023年6月6日
    0124
  • dockerfile

    基础结构 指令 from label maintainer run cmd export env add copy entrypoint volume user workdir o…

    数据库 2023年6月14日
    080
  • Java 考试系统项目源码 springboot mybaits vue.js 支持手机端考试

    新增功能:培训学习模块, PDF电子课程、视频课程、直播课程(自己搭建直播流服务器) 人脸识别(考试时验证,有开关)、补考开关 组建试卷:创建试卷,题目、类型、总分、及格分数、时长…

    数据库 2023年6月6日
    082
  • 2_爬豆瓣电影_ajax动态加载

    什么是 AJAX ? AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。 AJAX = Asynchronous JavaScript and XML(AJA…

    数据库 2023年6月11日
    0106
  • MySQL触发器

    触发器 先来个实际的案例 触发器概述 和存储过程一样,都是嵌入在MySQL服务器中的一段程序 触发器由 事件触发,比如INSERT ,UPDATE 等用户的动作或触发某项行为,自动…

    数据库 2023年5月24日
    099
  • 简单!代码原来是这样被CPU跑起来的

    CPU对我们来说既熟悉又陌生,熟悉的是我们知道代码是被CPU执行的,当我们的线上服务出现问题时可能首先会查看CPU负载情况。陌生的是我们并不知道CPU是如何执行代码的,它对我们的代…

    数据库 2023年6月6日
    097
  • ElasticSearch的简单api介绍

    1:ElasticSearch是什么? Elasticsearch 是一个分布式的免费开源搜索和分析引擎 适用于包括文本、数字、地理空间、结构化和非结构化数据等在内的所有类型的数据…

    数据库 2023年6月6日
    082
  • 私人菜谱

    红烧排骨食材:排骨一斤半 花椒葱姜蒜 豆瓣酱 生抽 耗油 白糖 两个大土豆步骤:少量油 花椒葱姜蒜(大块儿)爆香 一大勺豆瓣酱 倒入排骨翻炒一会儿 倒入一锅铲生抽 一小撮耗油撒一撮…

    数据库 2023年6月6日
    091
  • CSS进阶内容—浮动和定位详解

    CSS进阶内容—浮动和定位详解 我们在学习了CSS的基本知识和盒子之后,就该了解一下网页的整体构成了 当然如果没有学习之前的知识,可以到我的主页中查看之前的文章: CSS的三种布局…

    数据库 2023年6月14日
    078
  • 汇编实验二设置栈顶

    实验笔记二:ss设置栈顶 mov ax,2000 mov ss,ax mov sp,0010 mov ax,2000 mov ss,ax mov sp,0010 执行后,内存地址会…

    数据库 2023年6月11日
    077
  • 基于 ZooKeeper 的分布式锁实现

    ZK 基本概念 apache hadoop 下面的子项目,是一个树形目录服务 字面意思就是动物管理员,诞生之初用来管理 hadoop(大象)、Hive(蜜蜂)、Pig(小猪) 用于…

    数据库 2023年6月6日
    088
  • MAC MySQL安装配置

    1. 下载 注意选择对应的版本,M系列芯片对应ARM 2. 安装 一直点击继续即可, 注意要记住root用户端密码 3. 配置 在 ~/.bash_profile 增加 4. 服务…

    数据库 2023年6月14日
    0106
  • form表单上传文件及后端获取文件

    写在前面(强调) 使用form表单上传文件时必须要干的两件事: 1.form&#x8868;&#x5355;&#x7684;method&#x630…

    数据库 2023年6月14日
    0136
  • 西数数码-安装hmx_linux下的环境记录

    [nginx]name=nginx repobaseurl=http://nginx.org/packages/centos/6/x86_64/gpgcheck=0enabled=…

    数据库 2023年6月14日
    060
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球