HashTable源码分析

1.前言

Hashtable 一个元老级的集合类,早在 JDK 1.0 就诞生了

1.1.摘要

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

1.2.简介

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 {    .....}

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 方法之后,又做了一些高位运算,也就是第二步方法,来打散数据,让哈希的结果更加均匀。

1.3.常用方法介绍

1.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+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<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关键字,所以,在同步操作的时候,是线程安全的。

1.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关键字,所以,在同步操作的时候,是线程安全的。

1.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关键字,所以,在同步操作的时候,是线程安全的。

1..3.4.总结

总结一下 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. 并发-HashMap和HashTable源码分析

    HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...

  2. Java入门系列之集合Hashtable源码分析(十一)

    前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...

  3. 在linux操作系统上进行简单的C语言源码的gcc编译实验

    尝试在linux上用gcc 而非封装完好的codeblocks,vs等ide 来编译c和cpp源程序 首先查看我的gcc版本,我的是VM centos 自带的,没有的话得自行安装,安装上gcc就可以在 ...

  4. JAVA的HashTable源码分析

    Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长.Hashtable ...

  5. Java基础——HashTable源码分析

    HashTable是基于哈希表的Map接口的同步实现 HashTable中元素的key是唯一的,value值可重复 HashTable中元素的key和value不允许为null,如果遇到null,则返 ...

  6. Java - HashTable源码分析

    java提高篇(二五)-----HashTable 在java中与有两个类都提供了一个多种用途的hashTable机制,他们都可以将可以key和value结合起来构成键值对通过put(key,valu ...

  7. HashTable源码分析

      本次分析代码为JDK1.8中HashTable代码.   HashTable不允许null作为key和value.   HashTable中的方法为同步的,所以HashTable是线程安全的. E ...

  8. HashMap和HashTable源码分析

    HashMap HashMap是一个实现了Map接口的Hash表.提供所有Map的操作,并且允许null key和null value.HashMap几乎等同于HashTable,只不过HashMap ...

  9. java.util.Hashtable源码分析

    Hashtable实现一个键值映射的表.任何非null的object可以用作key和value. 为了能存取对象,放在表里的对象必须实现hashCode和equals方法. 一个Hashtable有两 ...

随机推荐

  1. C# 强转空会不会出现异常

    有小伙伴问我强转 null 会不会出现异常,我告诉他,如果是引用类型那么不会,如果是值类型,那么会出现空异常 如果是引用类型,只要是空类型,是支持随意转换,如下面代码,这是可以运行 class Pro ...

  2. 【Docker】企业级镜像仓库harbor的搭建(http/https)及使用

    一:用途 Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器. 二:安装docker-ce 环境:阿里云轻量应用服务器CentOS 7.3 这里通过yum Docker源仓 ...

  3. 一篇文章带你了解 ZooKeeper 架构

    上一篇文章,我们讲解了 ZooKeeper 入门知识,这篇文章主要讲解下 ZooKeeper 的架构,理解 ZooKeeper 的架构可以帮助我们更好地设计协同服务. 首先我们来看下 ZooKeepe ...

  4. 使用 Postman 测试你的 API

    使用 Postman 测试你的 API Intro 最近想对 API 做一些自动化测试,看了几个工具,最后选择了 postman,感觉 postman 的设计更好一些,我们可以在请求发送之前和请求获取 ...

  5. nginx负载均衡的相关配置

    一台nginx的负载均衡服务器(172.25.254.131) 两台安装httpd作为web端 一.准备工作 1.1 安装nginx yum -y install gcc openssl-devel ...

  6. 深入浅出 Viewport 设计原理

    Viewport 是 HTML5 针对移动端开发新增的一个 meta 属性, 它的作用是为同一网页在不同设备的呈现,提供响应式解决方案.这篇文章尝试通过循序渐进的方式,逐层探索 Viewport 的设 ...

  7. 【题解】ARC101F Robots and Exits(DP转格路+树状数组优化DP)

    [题解]ARC101F Robots and Exits(DP转格路+树状数组优化DP) 先删去所有只能进入一个洞的机器人,这对答案没有贡献 考虑一个机器人只能进入两个洞,且真正的限制条件是操作的前缀 ...

  8. 【题解】[HAOI2018]染色(NTT+容斥/二项式反演)

    [题解][HAOI2018]染色(NTT+容斥/二项式反演) 可以直接写出式子: \[ f(x)={m \choose x}n!{(\dfrac 1 {(Sx)!})}^x(m-x)^{n-Sx}\d ...

  9. 认识Web应用框架

    Web应用框架 Web应用框架(Web application framework)是一种开发框架,用来支持动态网站.网络应用程序及网络服务的开发.类型可以分为基于请求(request-based)的 ...

  10. 「CF1082C」Multi-Subject Competition 解题报告

    人生第一篇题解,虽然这道题做的人暂时不多,但我相信它--迟早有一天会发扬光大的!!! 说完废话 步入正题 题意: 传送门 思路: 模拟.枚举 对于每个组里的数字,先排序,然后从一到最大可能的情况,枚举 ...