首先来一波对比,Hashtable与HashMap,这两个一看到,Hashtable是线程安全,put、remove、get、size、hashCode等方法都用了synchronized同步修饰符,而HashMap是非线程安全的。Hashtable很早(JDK1.0)就出现,从复杂度上来说,先研究一下Hashtable利于后续理解HashMap和ConcurentHashMap的设计。
看源码个人习惯于从代码入口看起,Hashtable构造函数,如下图:
有四个构造函数,默认参数大小 11和0.75f(分别代表初始大小和加载因子,继续往下看)
构造函数后,看到几个类变量成员,那这几个变量成员分别是啥,代表什么意思?
table:数据存储结果,一个数组链表,读取和删除等操作速度在数组和链表折中(扩容后续讨论)。
count:hashtable的元素数量,也就是hashtable大小。
threshold:判断Hashtable是否需要扩容的临界值。
loadFactor:加载因子,默认0.75,影响threshold的大小和扩容效率。
modCount:Hashtable的修改次数。
声明Hashtable实例后,则是put和get等操作:
1、看看put函数做了啥,Hashtable的value不能为空即是这里第一行做了判断,而key为空时第453报错导致。至此,可以明白Hashtable为什么key和value都不能为null了。而后继续往下看。
a)计算数组下表index,这里关注与table的length取余,后续扩容有影响。
b)for循环比较简单,通过下标找到数组对应的链表,然后判断是否有对应的key存在,存在则更新value并返回旧值。
c)执行addEntry新增元素。
2、addEntry函数新增元素,主要是判断是否count>=threshold需要扩容,需要则扩容;count是Hashtable大小,而threshold是Hashtable当前分配大小乘以一个负载因素,数据在entry[]中的分布可能如下:
可能1:entry[n],当前m个元素, 有m个链表,每个链表一个元素。(entry是一个数组链表的结构)
可能2:entry[n],当前m个元素,只有一个链表,这个链表存了m个元素。
可能3:entry[n],当前m个元素,有s(s<m)个链表,每个链表的元素数量>0。
从以上三个可能的元素分布中,会发现极端情况可能2是有些链表的深度太大,操作效率会降低,此时合理做法是将元素重新分散hash到不同链表,以使链表元素分布均匀。
遇到可能1的情况也是比较极端的,变成一个数组,就没有发挥链表的快速修改的优势。
最合理的分布式是 可能3 ,所以,在设置加载因子loadFactor的时候需要考虑应用场景和数据分布,在数据量比较小的时候可能会频繁的rehash,随着数据量增长应该是散列分布(不排除数据key值都是递增的情况)。
3、接下来看一下扩容rehash是怎么做的。
a)新容量 newCapacity = (oldCapacity << 1) + 1(不越界的情况可以理解成oldCapacity * 2 + 1)。
b)判断newCapacity 是否越界,如果越界而且原table数组已经是所允许的最大值,则什么都不做直接返回,否则扩容为最大值。
c)新建一个newMap对象存储新table,更新扩容临界threshold的值。
d)循环旧table的数组及其对应的链表,O(n)复杂度,重新hash计算index。
4、相对而言,获取元素的时候就简单了,而且速度也是比较快的,因为通过hash快速获取数组下标从而取到子链表,后续只需要遍历子链表,相对单纯链表而言速度无疑读取效率提升许多。
5、看看删除,毋庸置疑,删除也是很简单快速的,获取下标后遍历子链表,需要判断目标元素是链表头还是其他,就是移动一下引用而已,so easy。
6、还有个比较有意思就是hashCode方法。在计算hashCode之前做了loadFactor = -loadFactor; 的操作标记正在计算hashCode,看得到是会做loadFactor 是否小于0的判断,不过有个疑惑就是这个函数不是已经有synchronize同步修饰了方法吗,也没到看其他地方用loadFactor做计算,后续再看看,留待评论。