基于JDK1.8对Java中的Hashtable集合的源码进行了深度解析,包括各种方法、扩容机制、哈希算法、遍历方法等方法的底层实现,最后给出了Hashtable和HashMap的详细对比以及使用建议。

1 Hashtable的概述

public class Hashtable< K,V > extends Dictionary< K,V > implements Map< K,V >, Cloneable, Serializable

Hashtable是来自于JDK1.0时代的古老key-value形式的集合类。类当中所有的方法都是同步的,数据安全的,效率低。

JDK1.0的时候Hashtable是继承的抽象类Dictionary,JDK1.2集合框架诞生之后,又实现了Map 接口,成为了Java集合体系的一员。

实现了Cloneable、Serializable标志性接口,支持克隆、序列化操作。

由于Map不属于Collection集合体系,没有实现Iterable接口,因此不支持获取迭代器的方法iterator(),或者说Map的集合体系并没有真正的迭代器。但是它们有自己的遍历数据的方法。

Hashtable的底层实际上是采用“拉链法”实现了一个哈希表,即使用一个数组作为哈希表的骨架,每一个数组元素的位置称为“bucket”桶,桶里存放的就是哈希值相同的键值对,如果一个桶里面有多个键值对,那么说明出现了哈希冲突,Hashtable使用“拉链法”解决冲突,每个桶的大小即该位置链表节点数量。Hashtable的key 和 value 都不允许为null。

2 Hashtable的源码解析

2.1 主要类属性

/*** 内部Entry[]数组,用来作为哈希表的骨架,数组每一个Entry元素代表了一个链表的头节点,Hashtable内部的哈希表的key-value键值对都是存储在Entry节点中的。*/
private transient Entry<?, ?>[] table;/*** HashTable的大小,注意这个大小并不是HashTable的容器大小,而是他所包含Entry键值对的数量。*/
private transient int count;/*** Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。当count大于等于threshold时,需要调整容量(尝试扩容)。*/
private int threshold;/*** 加载因子,是可以大于1的。*/
private float loadFactor;/*** 用来实现"fail-fast"机制的(也就是快速失败)。*/
private transient int modCount = 0;

扩容阈值是由出初始容量和加载因子共同决定的,通常threshold=table.length*loadFactor,初始容量和加载因子越大,那么就不需要频繁的“扩容”,初始容量过大可能会浪费更多空间,加载因子越大会增加哈希冲突的风险,导致查找数据的时间过长。默认容量(11)和加载因子(0.75)在时间和空间成本上寻求一种折衷。

关于modCount 的作用和fail-fast机制,早在ArrayLsit集合的源码文章中就已经讲解了,java.util包下的集合的fail-fast机制都是一样的,这里不再赘述,详情可以看这篇文章:Java集合—ArrayList的源码深度解析以及应用介绍。

2.2 Entry节点

Entry实际上就是Hashtable的一个内部类,作为内部存储key和value的容器,还保存key的hashCode值,同时由于Hashtable采用“拉链法”实现哈希表,每一个Entry还作为链表的一个节点,因此内部还有一个到下一个节点的引用属性。

实际上Entry实现了Map.Entry接口,因此Entry内部还实现了相关方法共外部调用。EntrySet()方法返回的set集合的元素May.Entry,实际上就是返回的这个Entry节点的实例,后面会详细讲解!

private static class Entry<K,V> implements Map.Entry<K,V> {//哈希值,存储起来方便后续使用,避免重复运算final int hash;//keyfinal K key;//valueV value;//下一个相同桶位的节点引用Entry<K,V> next;protected Entry(int hash, K key, V value, Entry<K,V> next) {this.hash = hash;this.key =  key;this.value = value;this.next = next;}@SuppressWarnings("unchecked")protected Object clone() {return new Entry<>(hash, key, value,(next==null ? null : (Entry<K,V>) next.clone()));}public K getKey() {return key;}public V getValue() {return value;}public V setValue(V value) {if (value == null)throw new NullPointerException();V oldValue = this.value;this.value = value;return oldValue;}public boolean equals(Object o) {if (!(o instanceof Map.Entry))return false;Map.Entry<?,?> e = (Map.Entry<?,?>)o;return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&(value==null ? e.getValue()==null : value.equals(e.getValue()));}public int hashCode() {return hash ^ Objects.hashCode(value);}public String toString() {return key.toString()+"="+value.toString();}
}

据此,我们能够画出Hashtable的大概数据结构图:

2.3 构造器与初始化参数

2.3.1 Hashtable()

public Hashtable()

构造一个新的,空的散列表,默认初始容量(11)和加载因子(0.75)。

public Hashtable() {//内部调用另外一个构造器,初始容量11,加载因子0.75this(11, 0.75f);
}

2.3.2 Hashtable(int initialCapacity)

public Hashtable(int initialCapacity)

用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。

public Hashtable(int initialCapacity) {//内部调用另外一个构造器,用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。this(initialCapacity, 0.75f);
}

2.3.3 Hashtable(int initialCapacity, float loadFactor)

public Hashtable(int initialCapacity,float loadFactor)

用指定初始容量和指定加载因子构造一个新的空哈希表。加载因子可以大于1,但是很明显,加载因子越大,发生哈希冲突的概率也越大!

这里的initialCapacity也没有要求是2的幂次方,但是HashMap 中初始化容量大小必须是 2 的幂次方。

/*** 建议数组最大容量,因为某些VM实现可能需要部分长度用来存放数组头部信息* 但是在HotSopt的虚拟机中,数组长度是可以超过这个限制的,可以达到Integer.MAX_VALUE – 2的长度* 并且在上面的源码中能够看到,我们分配的initialCapacity完全可以大于MAX_ARRAY_SIZE*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;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);//如果初始容量为0,则变成1if (initialCapacity == 0)initialCapacity = 1;this.loadFactor = loadFactor;//创建数组table = new Entry<?, ?>[initialCapacity];//计算扩容阈值,取initialCapacity * loadFactor和MAX_ARRAY_SIZE + 1的最小值threshold = (int) Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

2.3.4 Hashtable(Map<? extends K,? extends V> t)

public Hashtable(Map<? extends K,? extends V> t)

构造一个与给定的 Map 具有相同映射关系的新哈希表。该哈希表是用足以容纳给定 Map 中映射关系的初始容量和默认的加载因子(0.75)创建的。

public Hashtable(Map<? extends K, ? extends V> t) {//首先初始化hashtablethis(Math.max(2*t.size(), 11), 0.75f);//底层调用putAll方法putAll(t);
}

2.4 put方法与扩容机制

public synchronized V put(K key, V value)

将指定 key 映射到此哈希表中的指定 value。如果key或value为 null,则抛出NullPointerException。

put方法的源码量比较多,并且是Hashtable中比较关键的一部分,但是并不难理解!我们分成三部分来讲解:put、addEntry、rehash。

2.4.1 put

开放给外部调用的put方法主要可以分为4步:

  1. 通过hash算法计算新键值对的位置;
  2. 判断该位置是否存在元素,以及是否存在key相同的元素;
  3. 如果存在,并且找到相同的key,那么替换value,返回旧值,方法结束;
  4. 如果不存在元素,或者没有找到相同的key,那么调用addEntry方法添加新元素节点,返回null,方法结束。
/*** 开放给外部调用的添加节点的方法,主要分4步:* 1、通过hash算法计算新键值对的位置* 2、判断该位置是否存在元素,以及是否存在key相同的元素* 3、如果存在,并且找到相同的key,那么替换value,返回旧值,方法结束* 4、如果不存在元素,或者没有找到相同的key,那么添加新元素节点,返回null,方法结束** @param key   键* @param value 值* @return 旧值*/
public synchronized V put(K key, V value) {/*1 通过hash算法计算新键值对所在位置*/// 确保value不为nullif (value == null) {throw new NullPointerException();}//获取table数组Entry<?, ?> tab[] = table;/*Hashtable的hash算法*///获取key的hash值,该方法就是Object中的方法,key也可以重写该方法,int hash = key.hashCode();//通过hash值进行相应的计算,确定key-value在table[]中存储的索引位置int index = (hash & 0x7FFFFFFF) % tab.length;//获取数组在该索引位置的元素entryEntry<K, V> entry = (Entry<K, V>) tab[index];/*2 判断该位置是否存在元素,以及是否存在key相同的元素如果entry不为null,则说明有元素,并且可能不只有一个元素*///那么迭代index索引位置的链表,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值for (; entry != null; entry = entry.next) {//判断key相等的方案法,首先要求两个key的hashCode()方法返回的hash值相等,然后要求两个key的equals方法返回true。if ((entry.hash == hash) && entry.key.equals(key)) {//如果相等则替换旧的value并返回V old = entry.value;entry.value = value;return old;}}//走到这一步,说明两种情况,一种是entry为null;另一种是entry不为null,但是并没有找到相同的key//此时说明需要添加节点,调用addEntry方法addEntry(hash, key, value, index);//旧值返回nullreturn null;
}

下面是关键源码分析:

首先是hashtable的哈希算法 ,从上面的源码中我们知道,Hashtable的hash算法是:

(hash & 0x7FFFFFFF) % tab.length;

其中hash就是key的hashCode方法的返回值,0x7FFFFFFF表示最大的int类型的数据,即2147483647,它的二进制表示就是除了首位符号位是 0,其余都是1。

由于hashCode方法返回的hash是int类型的整数,并可正可负,因此首先进行的hash & 0x7FFFFFFF的目的是将hash转换为一定是大于等于0的整数。

然后再对底层数组的长度取余,我们知道余数一定会比除数更小,一个大于等于0的被除数除以一个正整数的余数的范围一定是[0, 除数)之间的,即[0, tab.length-1]。

通过该hash算法计算出来的桶位置刚好能够覆盖整个数组的全部索引值,并且不会超出它的范围!

我们还能知道,由于最终桶位置是通过求余“%”计算出来的,那么如果被除数为奇数,即数组容量为奇数,此时求得的余数将会更加均匀(hash函数为什么要选择对奇数求余?),这也是后面的扩容算法(oldCapacity << 1) + 1的由来,加1之后新容量将变成奇数,但是,hashtable并没有强制保证容量一定是是质数,因为可以通过构造器方式设置容量,这可能是HashTable已经不被sun公司推荐使用了。

然后是判断重复key,这里的判断方法是:

(entry.hash == hash) && entry.key.equals(key)

即用了两步,首先判断两个key的hashCode的值是否相等,然后判断两个key的equals方法是否返回true!

最后如果不存在元素,或者没有找到相同的key,那么调用addEntry方法添加新元素节点,返回null,方法结束。

下面来看addEntry方法源码!

2.4.2 addEntry

添加新元素节点的方法addEntry又可以分为3步:

  1. 判断是否需要扩容;
  2. 如果需要扩容,那么调用rehash方法扩容,并且重新计算新key在新数组的位置;
  3. 采用头插法,插入节点,方法结束;
/*** 内部添加新节点的方法,主要分3步* 1、判断是否需要扩容;* 2、如果需要扩容,那么rehash()进行扩容,并且重新计算新key在行数组的位置;* 3、采用头插法,插入节点;** @param hash  hashcode方法获取到的key的hash值* @param key   键* @param value 值* @param index Hashtable的hash算法计算出来的键值对存放的位置*/
private void addEntry(int hash, K key, V value, int index) {//哈希表结构改变次数自增1,该值只与"fail-fast"机制有关modCount++;Entry<?, ?> tab[] = table;/*1 如果节点数量大于等于扩容阈值,此时开始扩容*/if (count >= threshold) {/*2 对于底层数组进行扩容以及内部的元素重新通过hash算法计算在新数组中的位置并移动到新数组中*/rehash();/*对于需要新增的k-v,同样要重新计算在新数组中的位置*/tab = table;hash = key.hashCode();index = (hash & 0x7FFFFFFF) % tab.length;}/*3 创建新的entry节点,添加到链表头部,使之成为新的头节点,即"头插法"*///获取数组索引处的节点,该节点实际上就是链表的头节点Entry<K, V> e = (Entry<K, V>) tab[index];//新建entry节点,next指向原来的该位置的头节点,新节点放入数组在该索引的位置中,成为新节点。tab[index] = new Entry<>(hash, key, value, e);//节点数量自增1count++;
}
复制代码

下面是关键源码分析:

首先是判断是否需要扩容的源码:

count >= threshold

这也就是threshold被称为扩容阈值的来源,元素数量大于等于该值就需要扩容。

然后使用rehash()方法扩容,之后注意,由于经过了扩容,数组长度可能发生了变化,那么还需要重新计算新节点的插入位置!

最后就是插入新节点,这里采用的“头插法”。所谓的“头插法”很简单,实际上就是将新点作为链表的头节点插入,在Haashtable里表示为:新节点存入数组对应索引位置,原索引位置的节点成为新节点的next节点!使用头插法主要是考虑到新插入的数据,更可能作为热点数据被使用,放在头部可以减少查找时间。

在JDK1.8之前的HashMap也是采用“头插法”插入元素节点,但是在JDK1.8时,改为“尾插法”,因为头插法在多线程操作时可能形成环形链表造成死循环,具体原理在Hashmap原理的文章中会有讲解,但是由于Hashtable是线程安全的,因此不需要改动!

下面单独来看看rehash()扩容方法!

2.4.3 rehash

扩容的方法rehash又可以分为2步:

  1. 数组扩容,即尝试建立一个更大的数组;
  2. 如果扩容成功,那么循环遍历旧的数组,转移节点到新数组,方法结束;
/*** 内部数组扩容方法以及数据转移机制,主要分两步:* 1、数组扩容* 2、循环遍历旧的数组,转移节点*/
protected void rehash() {/*1 数组扩容*///获取旧的容量int oldCapacity = table.length;//获取旧的数组引用Entry<?, ?>[] oldMap = table;//新的容量为 老的容量左移一位之后再加一,即oldCapacity*2+1int newCapacity = (oldCapacity << 1) + 1;//如果新容量减去MAX_ARRAY_SIZE大于0,这里要注意:新容量并不一定大于MAX_ARRAY_SIZE,也可能是负数if (newCapacity - MAX_ARRAY_SIZE > 0) {//如果老的容量等于MAX_ARRAY_SIZEif (oldCapacity == MAX_ARRAY_SIZE)// 那么继续使用老的容量继续运行,扩容结束,即达到了数组的最大容量,不再继续扩容了return;//否则新容量直接等于MAX_ARRAY_SIZEnewCapacity = MAX_ARRAY_SIZE;}//新建新容量的数组Entry<?, ?>[] newMap = new Entry<?, ?>[newCapacity];//数组结构改变次数加1modCount++;//计算新的扩容阈值threshold = (int) Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//table指向新的数组table = newMap;/*2 循环遍历旧数组,进行旧的数组元素节点的转移*/for (int i = oldCapacity; i-- > 0; ) {//循环每一个数组节点处的链表,进行节点转移操作for (Entry<K, V> old = (Entry<K, V>) oldMap[i]; old != null; ) {//获取索引处的一个old节点,使用e来保存,第一次获取的e就是该索引处的链表的头节点Entry<K, V> e = old;//获取old节点的下一个节点old = old.next;//计算老节点e在新数组中的索引位置int index = (e.hash & 0x7FFFFFFF) % newCapacity;/*下面两步,也是头插法的方式插入元素*///该节点的的下一个节点指向新数组的位置的头节点e.next = (Entry<K, V>) newMap[index];//新数组的头节点指向该节点newMap[index] = e;}}
}

下面是关键源码分析:

首先是尝试扩容的源码:

int newCapacity = (oldCapacity << 1) + 1;

上面的代码用于计算新容量,新的容量为老的容量左移一位之后再加一,这里的<<是3运算符,能加快运算速度,用十进制表示即:newCapacity=oldCapacity*2+1,关键是下面一段代码:

if (newCapacity - MAX_ARRAY_SIZE > 0)

这段代码用于判断是否“真的能够扩容以及是否需要重新分配新最大容量”,由于计算机的二进制运算法则,如果原本oldCapacity比较大,那么新的容量可能会小于0,从而导致意想不到的情况。

关于计算机二进制计算的坑可以看前面ArrayLsit的分析以及这篇文章:计算机进制转换详解以及Java的二进制的运算方法,在此不多赘述。

因此尝试扩容时,由于构造器中我们可以随意设置初始容量,那么根据oldCapacity的大小,可以分为三种情况:

  1. oldCapacity 位于[1, Integer.MAX_VALUE /2-4]

    1. 新容量newCapacity将为正数,同时if中的判断将为假。那么此时不会进入if代码块中,后续可以正常创建新数组进行扩容。 有趣的是,如果oldCapacity是Integer.MAX_VALUE /2-4,那么newCapacity正好是MAX_ARRAY_SIZE,if语句中计算的值正好为0。
  2. oldCapacity 位于[Integer.MAX_VALUE /2-3, Integer.MAX_VALUE -5]
    1. 由于计算机二进制计算的法则,新容量newCapacity可能为正也可能为负,但是if中的判断将一定为真。那么此时会进入if代码块中:如果oldCapacity等于MAX_ARRAY_SIZE,那么不进行扩容;否则新容量newCapacity等于MAX_ARRAY_SIZE,这么看起来,有可能是缩容而不是扩容(当oldCapacity大于MAX_ARRAY_SIZE的时候,新容量等于MAX_ARRAY_SIZE,即缩小了容量)。 如果初始容量设置为Integer.MAX_VALUE - 5,那么在扩容时,newCapacity计算的结果为-11,并且-11 - MAX_ARRAY_SIZE等于2147483646,大于0,此时将会以MAX_ARRAY_SIZE为容量建立数组,我们知道MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)是小于Integer.MAX_VALUE – 5的,这就是“缩容”的由来!
  3. oldCapacity位于[Integer.MAX_VALUE -4, Integer.MAX_VALUE]
    1. 新容量newCapacity还是负数,并且if中的判断将为假。那么此时不会进入if代码块中,后续创建新数组进行扩容时将会抛出异常! 如果初始容量设置为Integer.MAX_VALUE -4,那么在扩容时,newCapacity计算的结果为-9,并且-9 - MAX_ARRAY_SIZE等于-2147483648,小于0,此时将会以-9为容量建立新数组,导致NegativeArraySizeException异常。

然后是数组节点转移的部分:

  1. 从旧的数组尾部开始循环每一个桶位中的链表的每一个节点,采用“头插法”转移到新的数组相应的位置上。
  2. 这里是从链表头节点开始遍历、转移的,如果原来的链表中的节点在新数组中的位置还是一样,那么新数组中该链表节点的顺序是原链表顺序的倒序!

2.5 putAll方法

public synchronized void putAll(Map<? extends K, ? extends V> t)

将指定映射的所有映射关系复制到此哈希表中,这些映射关系将替换此哈希表拥有的、针对当前指定映射中所有键的所有映射关系。如果指定的映射为 null,则抛出NullPointerException。

public synchronized void putAll(Map<? extends K, ? extends V> t) {//内部处理方式非常简单,就是循环遍历参数集合,然后调用put方法一个个的添加节点for (Map.Entry<? extends K, ? extends V> e : t.entrySet())put(e.getKey(), e.getValue());
}

2.6 remove方法

public synchronized V remove(Object key)

从哈希表中移除该键及其相应的值。如果该键不在哈希表中,则此方法不执行任何操作。如果key为 null,则抛出NullPointerException。

remove方法比较简单,就是首先计算出key的桶位置,然后循环该位置的链表,找出相同key的节点,移除该节点并返回value,没找到就返回null。

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];//循环链表,查找key相同的节点,判断是否相同是通过key的hashcode和equals方法一起比较得出来的结果//使用prev来保存e的前驱for (Entry<K, V> prev = null; e != null; prev = e, e = e.next) {/*如果key相同,那么就算找到了*/if ((e.hash == hash) && e.key.equals(key)) {modCount++;//如果前驱不为nullif (prev != null) {//那么前驱的next节点指向e的next节点,删除e节点prev.next = e.next;} else {//否则,e.next节点作为头节点,删除e节点tab[index] = e.next;}//节点数量减少1count--;//返回e节点的valueV oldValue = e.value;//value置空,助于GC回收e.value = null;return oldValue;}}//但这一步说明没找到相同的key,返回nullreturn null;
}

2.7 get方法

public synchronized V get(Object key)

返回指定键所映射到的值,如果此映射不包含此键的映射,则返回 null。如果key为 null,则抛出NullPointerException。

get方法就更加简单了,处理过程就是计算key的hash值,判断在table数组中的索引位置,然后迭代链表,匹配直到找到相等的key返回返回value,若没有找到返回null。

public synchronized V get(Object key) {Hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;for (Hashtable.Entry<?,?> e = tab[index]; e != null ; e = e.next) {if ((e.hash == hash) && e.key.equals(key)) {return (V)e.value;}}return null;
}

2.8 clear方法

public synchronized void clear()

清空哈希表。循环将数组索引位置置空即可,后续GC将会收集没有引用到的链表。

public synchronized void clear() {Entry<?,?> tab[] = table;modCount++;//循环将数组索引位置置空即可,后续GC将会收集没有引用到的链表for (int index = tab.length; --index >= 0; )tab[index] = null;//count置为0count = 0;
}

2.9 遍历的方法

虽然Map体系下面的集合并没有更加高级的迭代器(类似于liisiterator那种可以在迭代器中增删改查数据的迭代器),但是他们也有自己的遍历和设置值的方法。

Hashtable共有四种遍历的方法,三种是基于Map接口实现的:entrySet()、keySet()、values(),一种是诞生时自身就具备的:elements()、keys()。

我们主要讲解来自Map接口的遍历方法,更古老的方法并不过多介绍:

public synchronized Enumeration elements() 返回此哈希表中value的枚举。 public synchronized Enumeration keys() 返回此哈希表中的键的枚举。

2.9.1 主要类属性

首次通过某些遍历的方法请求结果视图时,将会创建一个视图对象,并赋值给下面对应的字段保存起来。因为这些结果视图和Map底层的哈希表的直接关联的,对于哈希表的改变将会反映在结果视图的遍历中。因此后续调用相同的方法,直接返回已经生成的结果视图即可,不需要创建新的视图对象,非常的巧妙!

/*保存 keySet方法返回的结果视图*/
private transient volatile Set<K> keySet;
/*保存 entrySet方法返回的结果视图*/
private transient volatile Set<Map.Entry<K, V>> entrySet;
/*保存 values方法返回的结果视图*/
private transient volatile Collection<V> values;

下面的int类型常量主要是用于keySet、values、entrySet方法返回的视图集合,在这个视图集合获取迭代器时,实际上内部调用同一个获取迭代器的方法:getIterator(int type),返回的是同一个迭代器实现,主要是根据在构建迭代器时,传入的迭代器类型进行判断,并返回的不同的结果!具体判断规则在“Map.Entry<K,V>接口”一节部分有详解!

/*返回的迭代器集合类型*/
//keyset方法返回的set集合所使用的迭代器集合
private static final int KEYS = 0;
//values方法返回的Collection集合所使用的迭代器集合
private static final int VALUES = 1;
//entrySet方法返回的set集合所使用的迭代器集合
private static final int ENTRIES = 2;

2.9.2 entrySet方法

public synchronized Set<Map.Entry<K,V>> entrySet()

返回此Map中包含的key-value键值对的set集合。该集合支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作。即此set支持元素移除,可从映射中移除相应的映射关系,但它不支持 add 或 addAll 操作。

2.9.2.1 entrySet方法

先看看entrySet方法的源码:

public Set<Map.Entry<K, V>> entrySet() {//判断entrySet视图是否为nullif (entrySet == null)//如果为null则说明是第一次调用entrySet方法,那么创建试图对象并且赋值给entrySet字段entrySet = Collections.synchronizedSet(new EntrySet(), this);//返回entrySet视图对象return entrySet;
}

我们看到内部实际上调用的Collections集合工具类的synchronizedSet方法,该方法将会基于原集合进行包装,并返回新的一个同步的SynchronizedSet类型的包装集合,其操作元素的方法还是调用的传入的原始集合的方法,这里传入的集合是一个EntrySet。

2.9.2.2 EntrySet内部类

来看看EntrySet的源码:

private class EntrySet extends AbstractSet<Map.Entry<K, V>> {/*** 支持迭代器操作*/public Iterator<Map.Entry<K, V>> iterator() {return getIterator(ENTRIES);}/*** 不支持 add 或 addAll 操作,因为它的add方法是调用父类 AbstractSet的方法,而AbstractSet中add方法的实现是抛出异常*/public boolean add(Map.Entry<K, V> o) {return super.add(o);}/*** 支持contains操作,实际上底层就是操作的table数组*/public boolean contains(Object o) {if (!(o instanceof Map.Entry))return false;Map.Entry<?,?> entry = (Map.Entry<?,?>)o;Object key = entry.getKey();Hashtable.Entry<?,?>[] tab = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;for (Hashtable.Entry<?,?> e = tab[index]; e != null; e = e.next)if (e.hash==hash && e.equals(entry))return true;return false;}/*** 支持remove操作,实际上底层就是操作的table数组*/public boolean remove(Object o) {if (!(o instanceof Map.Entry))return false;Map.Entry<?,?> entry = (Map.Entry<?,?>) o;Object key = entry.getKey();Hashtable.Entry<?,?>[] tab = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings("unchecked")Hashtable.Entry<K,V> e = (Hashtable.Entry<K,V>)tab[index];for(Hashtable.Entry<K,V> prev = null; e != null; prev = e, e = e.next) {if (e.hash==hash && e.equals(entry)) {modCount++;if (prev != null)prev.next = e.next;elsetab[index] = e.next;count--;e.value = null;return true;}}return false;}/*** 支持size操作** @return*/public int size() {return count;}/*** 支持clear操作*/public void clear() {Hashtable.this.clear();}
}

EntrySet表示Map的键值对对象(Map.Entry<K, V>)的set集合。

这里的EntrySet实际上是一个Hashtable中的内部类,操作元素的方法,都是基于底层哈希表操作的。

支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作,此 set 支持元素移除,可从映射中移除相应的映射关系。

不支持 add 或 addAll 操作,因为它的add方法是调用父类 AbstractSet的方法,而AbstractSet中add方法的实现是抛出异常。

2.9.2.3 synchronizedSet方法

再来看看Collections.synchronizedSet的源码:

/*** 返回一个同步set集合** @param s     原集合* @param mutex 用来作为锁的对象* @return 同步的新集合, 是SynchronizedSet类型*/
static <T> Set<T> synchronizedSet(Set<T> s, Object mutex) {return new SynchronizedSet<>(s, mutex);
}/*** SynchronizedSet集合,的主要方法都是继承SynchronizedCollection的方法*/
static class SynchronizedSet<E> extends SynchronizedCollection<E> implements Set<E> {private static final long serialVersionUID = 487447009682186044L;SynchronizedSet(Set<E> s) {super(s);}SynchronizedSet(Set<E> s, Object mutex) {super(s, mutex);}public boolean equals(Object o) {if (this == o)return true;synchronized (mutex) {return c.equals(o);}}public int hashCode() {synchronized (mutex) {return c.hashCode();}}
}/*** SynchronizedCollection实现了同步集合的大部分方法,很容易就能看出来:* 它的同名方法全都是调用的传入的集合(第一个参数)的方法,并且使用传入的第二个参数作为锁对象,通过同步块的方法来最终实现同步的* 实际上这就是Java设计模式——"装饰设计模式"的应用*/
static class SynchronizedCollection<E> implements Collection<E>, Serializable {private static final long serialVersionUID = 3053995032091335093L;//第一个参数,原集合final Collection<E> c;  // Backing Collection//第二个参数作为锁final Object mutex;     // Object on which to synchronizeSynchronizedCollection(Collection<E> c) {this.c = Objects.requireNonNull(c);mutex = this;}SynchronizedCollection(Collection<E> c, Object mutex) {this.c = Objects.requireNonNull(c);this.mutex = Objects.requireNonNull(mutex);}/*** 装饰加强后的方法** @return*/public int size() {//同步块synchronized (mutex) {//调用被装饰集合的方法return c.size();}}public boolean isEmpty() {synchronized (mutex) {return c.isEmpty();}}public boolean contains(Object o) {synchronized (mutex) {return c.contains(o);}}//…………
}

从源码能看出来,实际上Collections.synchronizedSet方法,就是一个装饰设计模式的方法,传入一个EntrySet对象和this对象,然后返回一个SynchronizedSet对象,该对象内部保存了传入的两个参数,它的同名方法,底层还是调用EntrySet对象的同名方法,并且使用this对象作为锁,这样就完成了对EntrySet对象方法的装饰加强,实现了同步!

2.9.2.4 Map.Entry<K,V>接口

我们看到返回的set集合的元素是Map.Entry<K,V>类型,实际上该类型就是表示集合中的映射项(键-值对)。一个Map.Entry对象就表示一个键值对,那么这个键值对和Hashtable中的节点对象Entry有什么联系吗?

实际上,Map.Entry追溯到最顶层,它出现在Map接口,Entry作为Map接口的内部接口,现在我们猜也能猜出来,这个Entry接口实际上是作为Map集合体系中的节点的超级接口,Map的具体实现类的内部节点类均需要实现该Map.Entry接口。

到这里我们就是知道了,我们获取的Map.Entry对象实际上返回的是各个Map实现类的节点对象,在Hashtable中我们获取的是Entry节点(Entry内部类实现了Map.Entry接口),在HashMap中我们获取的是Node节点(Node内部类也实现了Map.Entry接口)……

Map.Entry接口提供的方法如下,实际上在前面的Entry节点内部类的介绍中已经说了,这些方法也是Entry节点的方法:

boolean

equals(Object o) 比较指定对象与此项的相等性。

K

getKey() 返回与此项对应的键。

V

getValue() 返回与此项对应的值。

int

hashCode() 返回此映射项的哈希码值。

V

setValue(V value) 用指定的值替换与此项对应的值(可选操作)。

Map.Entry是通的entrySet()方法获取的set集合的iterator(int type)方法获取到的。EntrySet的iterator方法实现如下:

public Iterator<Map.Entry<K,V>> iterator() {return getIterator(ENTRIES);
}

我们可以看到传入的类型是ENTRIES类型,那么该迭代器获取的元素类型将会是一个Entry。

实际上内部的Enumerator迭代器内部会根据传入的类型返回的不同的元素:

return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);

可以看到,如果是KEYS类型,那么返回entry节点的key;如果是VALUES类型那么就返回entry节点的value;否则,那就是ENTRIES,那么就直接返回entry节点。

2.9.3 keySet方法

public synchronized Set keySet()

返回此哈希表中key的set集合。

查看keySet的源码,可以发现和EntrySet的源码非常相似:

public Set<K> keySet() {if (keySet == null)keySet = Collections.synchronizedSet(new KeySet(), this);return keySet;
}

同样采用了装饰设计模式,只不过这里的被装饰的类变成了KeySet类。同样该集合支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作。即此set支持元素移除,可从映射中移除相应的映射关系,但它不支持 add 或 addAll 操作。

private class KeySet extends AbstractSet<K> {/*** KeySet集合迭代器的获取,可以看到传入的KEYS类型*/public Iterator<K> iterator() {return getIterator(KEYS);}/*** 支持size操作*/public int size() {return count;}/*** 支持contains操作,实际上是调用了外部类Hashtable的containsKey方法*/public boolean contains(Object o) {return containsKey(o);}/*** 支持remove操作,实际上是调用了外部类Hashtable的remove方法* 前面添加的Hashtable.this.前缀是为了引导调用外部类的同名方法*/public boolean remove(Object o) {return Hashtable.this.remove(o) != null;}/*** 支持clear操作,实际上是调用了外部类Hashtable的clear方法* 前面添加的Hashtable.this.前缀是为了引导调用外部类的同名方法*/public void clear() {Hashtable.this.clear();}
}

2.9.4 values方法

public synchronized Collection values()

返回此哈希表中value的 Collection 集合。

查看values的源码,可以发现和EntrySet、keySet的源码非常相似:

public Collection<V> values() {if (values==null)values = Collections.synchronizedCollection(new ValueCollection(), this);return values;
}

同样采用了装饰设计模式,只不过这里的被装饰的类变成了ValueCollection类。同样该集合支持iterator、Iterator.remove、Collection.remove、removeAll、 retainAll、和 clear 操作。即此Collection支持元素移除,可从映射中移除相应的映射关系,但它不支持 add 或 addAll 操作。

注意由于Map中的value是可能相等的,因此这里的Collection.remove方法移除的是找到的第一个相等value的键值对。

3 HashMap 和 Hashtable的异同与应用

3.1 基于JDK1.8的HashMap 和 Hashtable的异同

相同点:

  1. 都是Map接口实现类,属于Map体系的集合,都可以存放键值对,都属于哈希表的实现,存放的键值对都是无序的。

不同点:

  1. 总体情况: HashMap 是JDK1.2 新添加的类,类当中方法的所有实现都是异步的,数据不安全,效率高。Hashtable是JDK1.0固有的类,类当中所有方法的实现都是同步的,数据安全的,效率低;
  2. 是否允许null: HashMap 的key和value允许 null; Hashtable 的key和value不允许 null;
  3. 遍历方式: hashMap具有Map接口的3种遍历方式keySet()、values()、entrySet(),Hashtable除了具有具有Map接口的3种遍历方式之外,还有自己的keys()、elements()方法可以遍历Map;
  4. 初始容量: HashMap 的默认初始容量为11;Hashtable的默认初始容量为16。
  5. 哈希算法: HashMap的哈希算法是对key的hash值进行了扰动运算(JDK1.8是1次位运算 + 1次异或运算),然后用结果和(容量-1)做&运算。Hashtable的哈希算法是对key的hash值和int最大值(0x7FFFFFFF)进行&运算,然后用结果对容量求余%,并没有扰动运算,因此HashMap的元素分布更加均匀(扰动算法能够让哈希值分部的更加规律)。
  6. 扩容增量: Hashtable扩容之后的容量是原容量的两倍加1。HashMap扩容之后的容量是原容量的两倍。HashMap的容量要求必须为2的幂次方,无论是初始容量还是扩容后的容量,Hashtable的初始化容量则没有要求。
  7. 数据结构: JDK1.8中HashMap使用了数组+链表+红黑树的数据结构实现哈希表,而Hashtable而是使用了数组+链表的数据结构实现哈希表,HashMap的实现更加复杂,但是查找效率更高。
  8. 插入节点方式: JDK1.8中HashMap使用“尾插法“插入新节点,而Hashtable使用“头插法”插入新节点。实际上在JDK1.8之前的HashMap也是采用头插法插入元素节点,但是在JDK1.8时,改为尾插法,因为头插法在多线程操作时可能形成环形链表造成死循环,但是由于Hashtable是线程安全的,因此不需要改动!

3.2 HashMap 和 Hashtable的应用

在单线程环境下,推荐使用HashMap,因为没有同步,以及底层数据结构更加先进,速度更快;在并发环境下,不能使用HashMap,但是也不推荐使用Hashtable,因为HashTable锁住的是整个方法,锁粒度太大,只有一把锁,严重影响性能,推荐使用JUC包下面的ConcurrentHashMap,它采用Lock和CAS机制,降低锁粒度,具有多把锁,提升了并发量!

万字长文|Hashtable源码深度解析以及与HashMap的区别相关推荐

  1. Java LockSupport以及park、unpark方法源码深度解析

    介绍了JUC中的LockSupport阻塞工具以及park.unpark方法的底层原理,从Java层面深入至JVM层面. 文章目录 1 LockSupport的概述 2 LockSupport的特征和 ...

  2. RocketMQ源码(十)—Broker 消息刷盘服务GroupCommitService、FlushRealTimeService、CommitRealTimeService源码深度解析

    深入的介绍了broker的消息刷盘服务源码解析,以及高性能的刷盘机制. 学习RocketMQ的时候,我们知道RocketMQ的刷盘策略有两个,同步或者是异步: 1. 同步刷盘:如上图所示,只有消息真正 ...

  3. HashMap源码深度解析【重点】

    [重点]HashMap源码深度解析 摘要 哈希表 哈希冲突 HashMap 数据结构 数据结构 HashMap实现原理 构造方法 PUT实现 HashMap的数组长度一定是2的次幂 get原理 Has ...

  4. dubbo源码深度解析_Spring源码深度解析:手把手教你搭建Spring开发环境

    Spring环境搭建流程,如果是第一次接触spring源码的环境搭建,确实还是比较麻烦的. 作者使用的编译器为目前流行的lntelliJ IDEA,版本为2018旗舰版.Eclipse用户还需要自己揣 ...

  5. 《Spring源码深度解析》 PDF

    Spring源码深度解析 PDF 下载 下载地址:https://pan.baidu.com/s/1o9qEwXW 密码:vwyo 转载:http://download.csdn.net/detail ...

  6. Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

    原文 Go netpoll I/O 多路复用构建原生网络模型之源码深度解析 导言 Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go ...

  7. Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean定义(一)

    我们在之前的博客 Spring源码深度解析(郝佳)-学习-ASM 类字节码解析 简单的对字节码结构进行了分析,今天我们站在前面的基础上对Spring中类注解的读取,并创建BeanDefinition做 ...

  8. 《Spring源码深度解析 郝佳 第2版》AOP

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  9. 《Spring源码深度解析 郝佳 第2版》ApplicationContext

    往期博客: <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度 ...

最新文章

  1. Android 使用adb 命令截图 的方法
  2. 绘制测试集、训练集的每一个病人或者样本的raidomics signiture图(绘制raidomics signature图),以及ROC曲线图
  3. keras-vis可视化特征
  4. tflearn 数据集太大无法加载进内存问题?——使用image_preloader 或者是 hdf5 dataset to deal with that issue...
  5. Request.UrlReferrer详解
  6. 由mysql分区想到的分表分库的方案
  7. Linux系统服务(systemctl)的使用
  8. Python进阶_wxpy学习:实用组件
  9. 15.4.5 简化元组的使用
  10. 【Makefile由浅入深完全学习记录6】Makefile中变量的高级主题上
  11. 超多的CSS3圆角渐变网页按钮
  12. jsoup Java HTML解析器
  13. 「leetcode」1356.根据数字二进制下1的数目排序【如何计算二进制中1的数量】详解!
  14. 7种常见的音频格式简析 MP3,WMA,WAV,APE,FLAC,OGG,AAC
  15. 哔哩哔哩电脑网页版怎么下载视频
  16. 饭谈:失眠,还有梦魇,第二天要上班应该怎么办?
  17. 干货 | 产品助理入门攻略(一枚入行3年的PM内心独白)
  18. [Shell命令] tar -cvf -xvf 打包解包文件夹
  19. 【饭谈】:开发说他要是不写bug,测试就会失业了。
  20. Inkscape如何将png图片转换为svg图片并且不失真

热门文章

  1. ubuntu 16.10下软件记录
  2. Outlook 解决“附件大小超出了允许的范围”
  3. 知识图谱与自然语言生成NLG
  4. 电子商务企业品牌推广的逻辑与方法
  5. 怎么能让小程序的排名靠前
  6. AUTOCAD(三)基础知识
  7. selenium和webdriver区别
  8. 关于wasm micro runtime堆的大小的指定
  9. Html5的form表单案例
  10. c语言驾照考试管理系统,交管网考试系统