史上最简单的的HashTable源码分析
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源码分析的更多相关文章
- 并发-HashMap和HashTable源码分析
HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...
- Java入门系列之集合Hashtable源码分析(十一)
前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...
- 在linux操作系统上进行简单的C语言源码的gcc编译实验
尝试在linux上用gcc 而非封装完好的codeblocks,vs等ide 来编译c和cpp源程序 首先查看我的gcc版本,我的是VM centos 自带的,没有的话得自行安装,安装上gcc就可以在 ...
- JAVA的HashTable源码分析
Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长.Hashtable ...
- Java基础——HashTable源码分析
HashTable是基于哈希表的Map接口的同步实现 HashTable中元素的key是唯一的,value值可重复 HashTable中元素的key和value不允许为null,如果遇到null,则返 ...
- Java - HashTable源码分析
java提高篇(二五)-----HashTable 在java中与有两个类都提供了一个多种用途的hashTable机制,他们都可以将可以key和value结合起来构成键值对通过put(key,valu ...
- HashTable源码分析
本次分析代码为JDK1.8中HashTable代码. HashTable不允许null作为key和value. HashTable中的方法为同步的,所以HashTable是线程安全的. E ...
- HashMap和HashTable源码分析
HashMap HashMap是一个实现了Map接口的Hash表.提供所有Map的操作,并且允许null key和null value.HashMap几乎等同于HashTable,只不过HashMap ...
- java.util.Hashtable源码分析
Hashtable实现一个键值映射的表.任何非null的object可以用作key和value. 为了能存取对象,放在表里的对象必须实现hashCode和equals方法. 一个Hashtable有两 ...
随机推荐
- 关于MySQL中查询大数据量的情况下分页limit的性能优化
https://blog.csdn.net/weixin_37848710/article/details/80772725
- 【Linux】sed笔记
sed - stream editor for filtering and transforming text(用于过滤和转换文本的SED流编辑器),主要是以行为单位进行处理,可以将数据行进行替换. ...
- 使用 HttpClient 进行表单提交时,遇到的问题
问题 在开发微信支付的小微商户进件接口时,需要通过表单来上传身份证图片等数据.在微信支付接口文档也说明了,需要使用 multipart/form-data 的方式发送请求..NET 提供了 Multi ...
- 利用OGG进行数据库表的初始化
OGG初始化 1.停止目标端re1进程 2.配置ei进程(若目标端有ei进程之前投递的文件则先清空) GGSCI (localhost) 1>add extract ei1, sourceist ...
- 用WPF实现大数据分析,超炫的效果,还带地图
开头语 经过一段时间研究,终于实现CS和BS相同效果的大数据展示平台了.首先来看看实现的效果,超炫的效果,客户特别喜欢,个人也非常满意,分享给各位,同大家一起交流学习. 大数据展示平台 从上图可以看出 ...
- 使用SonarQube+Eclipse来分析python代码
背景 最近在项目中推广集成测试的理念以及相关工具,在jenkins中集成sonar去分析项目的java代码的时候,意外的发现,sonarqube上还有对应python的插件,而自己写的测试工具大部分都 ...
- 听说你的 IDEA 昨天掉链子了?松哥给你准备了大招
昨天一直在忙,中午抽空瞅了一眼技术群,天呐,竟然都在切磋 IDEA 激活码的事情,瞬间明白可能 jetbrains 又在搞事情了. 我大概了解了下,这次出事的主要是 2019 版,之前的 2018 版 ...
- TCP状态机:当服务端主动发FIN进TIME_WAIT,客户端源端口复用会发生什么
0X01 正常情况下TCP连接会通过4次挥手进行拆链(也有通过RST拆除连接的可能,见为什么服务器突然回复RST--小心网络中的安全设备),下图TCP状态机展示了TCP连接的状态变化过程: 我们重点看 ...
- Eclipse直接运行算法第4版例子(重定向和读取指定路径文件)
Eclipse直接运行算法第4版例子(重定向和读取指定路径文件) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://b ...
- 【转】8 个效果惊人的 WebGL/JavaScript 演示
英文原文:9 IMPRESSIVE WEBGL JAVASCRIPT EFFECT SHOWCASE,翻译:iteye WebGL 是一种 3D 绘图标准,这种绘图技术标准允许把 JavaScript ...