带你深入浅出的分析 HashTable 源码

Hashtable 一个元老级的集合类,早在 JDK 1.0 就诞生了,今天小编想和大家一起来揭开它的面纱!

01、摘要

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

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

02、简介

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

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

public class Hashtable<K,V>extends Dictionary<K,V>implements Map<K,V>, Cloneable, java.io.Serializable {.....
}

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

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {.....
}

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

/*** <strong>NOTE: This class is obsolete.  New implementations should* implement the Map interface, rather than extending this class.</strong>*/
public abstract
class Dictionary<K,V> {......
}

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

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

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

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

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

与 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<K,V> implements Map.Entry<K,V> {/**hash值*/final int hash;/**key表示键*/final K key;/**value表示值*/V value;/**节点下一个元素*/Entry<K,V> 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+1int newCapacity = (oldCapacity << 1) + 1;//省略部分代码......Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
}

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

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

final Node<K,V>[] resize() {int newCap = 0;//部分代码省略......newCap = DEFAULT_INITIAL_CAPACITY;//默认容量为 16Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
}

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

final Node<K,V>[] resize() {//获取旧数组的长度Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int newCap = 0;//部分代码省略......//当进行扩容的时候,容量为 2 的倍数newCap = oldCap << 1;Node<K,V>[] newTab = (Node<K,V>[])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 分析

03、常用方法介绍

3.1、put方法

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

put 流程图如下:

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

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<K,V> e = (Entry<K,V>) tab[index];tab[index] = new Entry<>(hash, key, value, e);count++;
}

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

protected void rehash() {int oldCapacity = table.length;Entry<?,?>[] oldMap = table;//每次扩容为原来的 2n+1int 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<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;}}
}

总结流程如下:

  • 1、通过 key 计算对象存储在数组中的下标;

  • 2、如果链表中有 key,直接进行新旧值覆盖处理;

  • 3、如果链表中没有 key,判断是否需要扩容,如果需要扩容,先扩容,再插入数据;

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

3.2、get方法

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

get 流程图如下:

打开 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 的 remove 方法,源码如下:

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

04、总结

总结一下 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 已经被淘汰了,不要在新的代码中再使用它。

05、参考

1、JDK1.7 & JDK1.8 源码

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


【END】

关注下方二维码,订阅更多精彩内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546510.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Debian11服务器系统默认不存在sudo命令的解决办法

1.切换到root用户&#xff0c;安装vim&#xff0c;修改镜像源文件 apt-get install vim vim /etc/apt/sources.list注释掉原有的内容&#xff0c;添加如下阿里云的镜像源 deb http://mirrors.aliyun.com/debian/ bullseye main contrib deb-src http://mirrors.aliyun.com/…

Redis使用不当导致应用卡死

作者&#xff1a;小木来源&#xff1a;http://rrd.me/ezfTj首先说下问题现象&#xff1a;内网sandbox环境API持续1周出现应用卡死&#xff0c;所有api无响应现象刚开始当测试抱怨环境响应慢的时候 &#xff0c;我们重启一下应用&#xff0c;应用恢复正常&#xff0c;于是没做处…

阿里巴巴Linux开源镜像站 华为Linux开源镜像站 Linux宝塔面板

学习Linux系统和用Linux系统作服务器时&#xff0c;推荐Linux从Linux系统开源镜像站下载&#xff0c;国内源下载速度快&#xff0c;Linux系统可选择方案多&#xff01; 1.阿里巴巴开源镜像站&#xff1a; https://developer.aliyun.com/mirror/ 2.华为开源镜像站&#xff1a; …

阿里Java开发手册——如何优化数据库?

作者&#xff1a;杨冠宝/高海慧来自&#xff1a;码出高效 Java 开发手册数据库作为服务器端最为最为昂贵的资源之一&#xff0c;如果使用不当常常会导致系统卡顿或系统崩溃&#xff0c;那如何来优化数据库呢&#xff1f;下面来看阿里巴巴《Java开发手册》给出的优化方案。1 建立…

虚拟机VMware Workstation安装Linux服务器Debian11系统详细教程

虚拟机VMware Workstation安装Linux服务器Debian11系统详细教程如下&#xff1a; 准备好&#xff1a; 1.安装好虚拟机VMware Workstation 2.从Linux开源镜像下载的debian11镜像系统 从创建新的虚拟机开始 安装完系统重启进入系统&#xff0c;首先要做的是更新为阿里云的…

借力 Docker ,三分钟搞定 MySQL 主从复制!

hello 各位小伙伴大家好&#xff0c;今年 5 月份的时候&#xff0c;松哥和大家聊过如何搭建 MySQL 主从复制&#xff1a;提高性能&#xff0c;MySQL 读写分离环境搭建(一)提高性能&#xff0c;MySQL 读写分离环境搭建(二)不过很多小伙伴反映在 Linux 中安装 MySQL 比较费劲&…

阿里云服务器Debian11系统安装Linux宝塔面板 搭建WordPress个人博客

阿里云服务器可以使用Debian11镜像系统&#xff0c;在安装Linux宝塔面板&#xff0c;配置LNMP(LinuxNginxMySQLPHP&#xff09;后&#xff0c;再搭建WordPress个人博客。 一、安装Linux宝塔面板&#xff1a; https://www.bt.cn/ 复制debian安装linux宝塔面板命令&#xff0c…

面试官:聊一下你对MySQL索引的理解?

作者&#xff1a;浪人来源&#xff1a;http://t.cn/AiKmcEefMySQL索引&#xff1f;这玩意儿还能简单聊&#xff1f;明显是在挖坑&#xff0c;幸好老夫早有准备&#xff0c;切听我一一道来。 一、索引是什么?索引是帮助MySQL高效获取数据的数据结构。二、索引能干什么?索引非常…

Linux新建用户可以在shell中切换到该用户也能登录到图形桌面

解决使用useradd testname&#xff0c;passwd testname命令确实创建了一个用户&#xff0c;可以在shell中切换到该用户。但不能登录到图形桌面的问题&#xff01; 通过下面方法可以在Linux中新建用户&#xff0c;并创建用户主目录&#xff0c;也能登录到图形桌面 1.普通用户先…

这样写Java,同事直呼666

作者&#xff1a;涛姐涛哥来源&#xff1a;cnblogs.com/taojietaoge/p/11575376.html一、MyBatis 不要写 11当遇到多个查询条件&#xff0c;使用where 11 可以很方便的解决我们的问题&#xff0c;但是这样很可能会造成非常大的性能损失&#xff0c;因为添加了 “where 11 ”的过…

Lync Server 2010 安装部署系列三:添加DNS记录

为了让客户端能够自动登录系统&#xff0c;我们需要在DNS服务器上设置创建相关的SRV记录&#xff0c;首先在DNS服务器上创建一条A记录pool.Contoso.com对应IP地址为192.168.1.11&#xff0c;然后创建一条SRV记录对应刚才创建的A记录&#xff0c;服务的内容为_sipinternaltls&am…

Linux系统下MySQL导出数据库和导入数据库的命令

一、MySQL导出数据库用mysqldump命令 注意mysql的安装路径&#xff0c;即此命令的路径&#xff0c;如果你只要查询mysql的运行文件所在地址&#xff0c;直接用下面的命令就可以了&#xff0c;再切换到mysql的运行文件的路径 which mysqlcd /usr/bin/1、导出数据和表结构 命令格…

工作中 99% 会用到的 Git 命令

作者&#xff1a;命中水来源&#xff1a;https://www.cxiansheng.cn/daily/4901.分支操作git branch 创建分支git branch -b 创建并切换到新建的分支上git checkout 切换分支git branch 查看分支列表git branch -v 查看所有分支的最后一次操作git branch -vv 查看当前分支git b…

JSP tomcat 更新不生效

2019独角兽企业重金招聘Python工程师标准>>> 今天遇到一问题&#xff0c;更改好的jsp部署到tomcat后&#xff0c;无论如何页面也不生效&#xff0c;最后才知道是tomcat的work目录没有清的原因&#xff0c;借此机会也特地看了下work目录的作用以及原理&#xff0c;做…

Win10专业版系统Docker安装、配置和使用详细教程

一、win10专业版系统首先需要开启硬件虚拟化及Hyper-V功能&#xff0c;才能进行Docker for Windows软件安装。 如何开启硬件虚拟化&#xff0c;自行百度。可在任务栏中查看虚拟化是否开启。 win10系统&#xff0c;打开控制面板-“应用”-“程序和功能”&#xff0c;开启Hyper…

框架开发之Java注解的妙用

作者&#xff1a;locality来源&#xff1a;https://www.jianshu.com/p/b560b30726d4如果你还不会使用注解&#xff0c;你肯定不好意思对别人说你学过Spring&#xff0c;你学过Mybatis&#xff0c;因为它们用了大量的注解。可见注解在开发领域已经使用的非常广泛了。注解的好处&…

Win10专业版系统PyCharm专业版使用WSL(ubuntu20.04 LTS)配置Docker解释器配置环境详细教程

提前准备好环境: Win10x64专业版21H2; WSL的ubuntu20.04系统; PyCharm2020专业版; Docker for Windows软件稳定版。 一、Win10系统安装WSL(ubuntu20.04 LTS)子系统 1.按照下图,开启“适用于Linux的Windows子系统”和“虚拟机平台”功能,按照提示重启计算机。 2.在Mic…

Mybatis:颠覆你心中对事务的理解

作者&#xff1a;祖大俊来源&#xff1a;my.oschina.net/zudajun/blog/6667641.说到数据库事务&#xff0c;人们脑海里自然不自然的就会浮现出事务的四大特性、四大隔离级别、七大传播特性。四大还好说&#xff0c;问题是七大传播特性是哪儿来的&#xff1f;是Spring在当前线程…

Docker镜像和容器常用命令

一、.Docker帮助命令 1.显示docker的版本信息 docker version 2.显示docker的系统信息&#xff0c;包括镜像和容器的数量 docker info3.docker帮助命令 docker 命令 --help二、Docker镜像命令 1.查看所有本地的主机上的镜像 docker images实例测试&#xff1a; 2.搜索镜像…

如何学会阅读源码?

作者 | youzhibing链接 | cnblogs.com/youzhibing/p/9553752.html1.读源码的经历刚参加工作那会&#xff0c;没想过去读源码&#xff0c;更没想过去改框架的源码&#xff1b;总想着别人的框架应该是完美的、万能的&#xff0c;应该不需要改&#xff1b;另外即使我改了源码&…