博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Hashtable源码分析(基于jdk1.8,推荐)
阅读量:4035 次
发布时间:2019-05-24

本文共 8287 字,大约阅读时间需要 27 分钟。

Hashtable也算是集合系列中一个比较重要的集合类了,不过在介绍Hashtable的时候,总是不可避免的谈到HashMap,在面试的时候Hashtable往往也会结合HashMap一块来问。这篇文章就来好好地分析一下Hashtable

一、认识Hashtable

1、继承关系

为了能好好的理解Hashtable,我们先看一下他在整个集合体系中的位置:

在这里插入图片描述

从上面的图我们会发现,Hashtable和HashMap师出同门,不过这张图太宏观,我们还是放小了看:

在这里插入图片描述

这张图已经很清晰了,继承了Dictionary,实现了Map接口。

2、与HashMap的区别

如果你之前看过我写的那篇HashMap文章的话,在这里对他们俩的区别一定有了解,现在我们对其进行一个整理(这里只看区别):

(1)HashMap允许key和value为空,但是Hashtable不允许。

(2)Hashtable是线程安全的,HashMap不是线程安全。

(3)ashtable继承自Dictionary,HashMap继承自AbstractMap。

(4)迭代器不同,Hashtable是enumerator迭代器,HashMap是Iterator迭代器。

3、Hashtable基本使用

public class HashtableTest {
public static void main(String [] args){
//新建方式 Hashtable
table=new Hashtable<>(); Hashtable
table1=new Hashtable<>(16); Hashtable
table2=new Hashtable<>(16, 0.75f); HashMap
map=new HashMap<>(); Hashtable
table3=new Hashtable<>(map); table.put("张三", "1"); table.put("李四", "2"); //这种方式会出现空指针异常,因为Hashtable的key不能为空 table.put(null, "3"); System.out.println(table.toString()); }}

下面我们就通过源码来分析一下Hashtable。

二、源码分析

对于集合类的源码分析,一般都是从参数、构造方法、还有增删改查的基础上进行分析,然后就是增加元素,增多了怎么处理。删除元素,删多了怎么办等等。下面我们就按照这个思路一步一步分析:

1、参数

HashTable的底层采用"拉链法"哈希表,并且提供了5个主要的参数:

(1)table:为一个Entry[]数组类型,Entry代表了“拉链”的节点,每一个Entry代表了一个键值对。

(2)count:容器中包含Entry键值对的数量。

(3)threshold:阈值,大于这个阈值时需要调整容器容量。值=“容量*加载因子”。

(4)loadFactor:加载因子。这个比较重要。

(5)modCount:用来实现“fail-fast”机制的。对容器任何增删改操作都会修改modCount。如果出错立即抛出ConcurrentModificationException异常。

private transient Entry
[] table;private transient int count;private int threshold;private float loadFactor;private transient int modCount = 0;

上面的是源码,你会发现table、count、modCount还都是transient修饰的,这也就意味着这三个参数是不能被系列化的。

2、构造方法

下面我们看看其构造方法,源码中一共提供了4个构造方法:

(1)构造方法1

//构造一个空的Hashtable//默认容量是11,加载因子是0.75public Hashtable() {
this(11, 0.75f);}

(2)构造方法2

//在构造Hashtable的时候,指定容量//加载因子还是0.75public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);}

(3)构造方法3

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,那就赋值为1 if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; //新建一个table table = new Entry
[initialCapacity]; //设置阈值:initialCapacity * loadFactor和MAX_ARRAY_SIZE + 1的最小者 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);}

这个构造方法首先排除掉一些异常情况,然后新建一个table数组来装数据。

(4)构造方法4

//构造给定的 Map 具有相同映射关系的新哈希表:也就是下面这种//HashMap
map=new HashMap<>();//Hashtable
table3=new Hashtable<>(map);public Hashtable(Map
t) {
this(Math.max(2*t.size(), 11), 0.75f); putAll(t);}

这个构造方法我们可就要稍微注意了,真正实现这个操作的是putAll(t)。想要弄清楚我们不妨跟进去看看。

//把map通过for循环一个一个存放在另外一个map中public synchronized void putAll(Map
t) {
for (Map.Entry
e : t.entrySet()) put(e.getKey(), e.getValue());}

上面的代码使用了泛型,而且还是泛型通配符,不过意思很明确,就是通过for循环一个一个转移到新的map中。

以上所述就是整个构造方法的机制。

3、增加一个元素

public synchronized V put(K key, V value) {
//第一部分:确保value不为空 if (value == null) {
throw new NullPointerException(); } //第二部分:确保table中没有当前的key Entry
tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") 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;}

这些代码第一部分和第二部分都是为了没有异常,如果当前容器有这个key,那么直接以新值代替旧值即可,最主要的还是第三部分,添加一个元素的核心addEntry方法。进去看看:

private void addEntry(int hash, K key, V value, int index) {
//modCount是为了安全机制 modCount++; Entry
tab[] = table; if (count >= threshold) {
//如果大于阈值,就会重新hash扩容 rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } //没有异常情况,新建一个Entry根据hash值插入到指定位置即可 @SuppressWarnings("unchecked") Entry
e = (Entry
) tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++;}

上面的这些代码的大致意思就是如果容器里面没有满,那就新建一个Entry根据hash值插入到指定位置。而且一开始还提供了modCount确保安全(快速失败机制)。如何去扩容呢?下面我们接着讲。

2、扩容

扩容就是当容器中放满了,需要把容器扩大。我们看看这个rehash是如何扩容的。

protected void rehash() {
int oldCapacity = table.length; Entry
[] oldMap = table; //新容量=旧容量 * 2 + 1:实现方式就是oldCapacity << 1 int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE) return; newCapacity = MAX_ARRAY_SIZE; } //根据新容量建一个新Map,并赋值给table Entry
[] newMap = new Entry
[newCapacity]; modCount++; //重新计算阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; //将原来的元素拷贝到新的HashTable中 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; } }}

上面代码的注释已经很清楚了,不过上面相信你会有一个疑问,不管是put一个元素还是扩容,在计算hash的时候都出现了(e.hash & 0x7FFFFFFF) ,它的作用是什么呢?

你可以这样理解,hash值是int类型,而且一定是正数,和0x7FFFFFFF做与操作即是将负数变成正数,确保了获取到的index是正数。

3、删除一个元素

public synchronized V remove(Object key) {
Entry
tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry
e = (Entry
)tab[index]; 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;}

删除一个元素就比较简单,核心就是通过key计算在容器中的位置,然后把这个位置上的Entry删除即可。由于使用的链表删除起来会更简单。将前一个元素指针直接指向下一个元素,跳过当前元素e.next。

4、查询一个元素

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

这个就太简单了,通过key计算hash值找到位置,直接通过e.value获取值即可。

5、迭代器

与HashMap不同的是,它的迭代器是Enumeration。在这里我们不会讲解Enumeration,只是给出其基本的使用方法,因为Enumeration我会在专门的文章里面会介绍。这里就不再重复了。

public static void main(String[] args) {
Hashtable hashTable = new Hashtable(); hashTable.put("张三", "1"); hashTable.put("李四", "2"); hashTable.put("java的架构师技术栈", "3"); Enumeration e = hashTable.elements(); while(e.hasMoreElements()){
System.out.println(e.nextElement()); }}

这就是一个最简单的使用方法,

6、其他方法

public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException(); } Entry
tab[] = table; for (int i = tab.length ; i-- > 0 ;) {
for (Entry
e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true; } } } return false;}

这个方法是判断这个容器中是否有value这个值,判断的方法特别死板,就是通过for循环一个一个比较。

三、总结

说实话这个Hashtable使用的场景还是很局限的,所以一般情况下基本上不会用到,不管是效率还是空间特性。因为上面的增删改查方法你会发现,全是一个一个比对的,对于数据量大的时候这是非常耗时的,而且存储空间也是采用的链表。

一般来说非并发场景使用HashMap,并发场景可以使用Hashtable,但是推荐使用ConcurrentHashMap,因为它锁粒度更低、效率更高。

turn true;

}
}
}
return false;
}
``

这个方法是判断这个容器中是否有value这个值,判断的方法特别死板,就是通过for循环一个一个比较。

三、总结

说实话这个Hashtable使用的场景还是很局限的,所以一般情况下基本上不会用到,不管是效率还是空间特性。因为上面的增删改查方法你会发现,全是一个一个比对的,对于数据量大的时候这是非常耗时的,而且存储空间也是采用的链表。

一般来说非并发场景使用HashMap,并发场景可以使用Hashtable,但是推荐使用ConcurrentHashMap,因为它锁粒度更低、效率更高。

Hashtable往往会结合者HashMap来出问题,希望大家注意,最核心的还是HashMap。

在这里插入图片描述

转载地址:http://nnbdi.baihongyu.com/

你可能感兴趣的文章
socket,accept函数解析
查看>>
今日互联网关注(写在清明节后):每天都有值得关注的大变化
查看>>
”舍得“大法:把自己的优点当缺点倒出去
查看>>
[今日关注]鼓吹“互联网泡沫,到底为了什么”
查看>>
[互联网学习]如何提高网站的GooglePR值
查看>>
[关注大学生]求职不可不知——怎样的大学生不受欢迎
查看>>
[关注大学生]读“贫困大学生的自白”
查看>>
[互联网关注]李开复教大学生回答如何学好编程
查看>>
[关注大学生]李开复给中国计算机系大学生的7点建议
查看>>
[关注大学生]大学毕业生择业:是当"鸡头"还是"凤尾"?
查看>>
[茶余饭后]10大毕业生必听得歌曲
查看>>
gdb调试命令的三种调试方式和简单命令介绍
查看>>
C++程序员的几种境界
查看>>
VC++ MFC SQL ADO数据库访问技术使用的基本步骤及方法
查看>>
VUE-Vue.js之$refs,父组件访问、修改子组件中 的数据
查看>>
Vue-子组件改变父级组件的信息
查看>>
Python自动化之pytest常用插件
查看>>
Python自动化之pytest框架使用详解
查看>>
【正则表达式】以个人的理解帮助大家认识正则表达式
查看>>
性能调优之iostat命令详解
查看>>