结构
数组
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
哈希表
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组”
一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
特点
基于hash表的数据结构,实现了Map接口
它不同步 允许空值 不能保证Map的顺序,特别是它不保证该顺序恒久不变。
在桶中适当分散元素,集合视图需要的时间与“容量”成正比
初始容量
创建哈希表时的容量
负载因子
许哈希表的满度的量度,在它的容量自动增加之前得到
当数哈希表中的条目超过了负载因子和当前容量,哈希表是rehashed(即内部数据)结构被重建),使得哈希表大约有两倍桶数。
默认负载因子(0.75)提供了一个良好的时间和空间成本之间的权衡,更高的值减少了空间开销,但增加查找成本(反映在大多数操作,包括get()、put();
预期的entry的数量,和它的负载因子应考虑,在设置其初始容量,以使rehash操作数量最小化。
如果初始容量大于最大entry数量/负载因子,rehash不会发生
如果许多映射存储在HashMap实例中,创建它以一种足够大的容量比让它自动rehash当需要增长hash表的时候更有效。用许多hashCode相同的键 是一种稳定hash表性能的方法。
当key实现了Comparable接口,该类可以使用比较顺序帮助在key中间打破关系。
如何保证同步
HashMap不是同步的,如果多个线程并发地进入到一个hashmap 至少一个线程修改map的结构,必须在额外地实现同步,一个结构修改是任何操作,添加或者删除一个或多个mappings,只是改变值和key 相关的 实例包含的 不是一个结构化的修改,这通常是通过对一些自然封装Map的对象进行同步。
*如果不存在这样的对象,则应该使用“包装”来映射该映射。
{@link Collections#synchronizedMap Collections.synchronizedMap}方法,最好在创建时完成,以防止意外发生。
Map m = Collections.synchronizedMap(new HashMap(...));
迭代器
由该类的“集合视图方法”返回的迭代器是fail-fast的,如果map结构在iterator创建后被修改,在任何方式除了通过迭代器自己的remove方法,迭代器将会抛出一个ConcurrentModificationException异常,
因此,面对并发修改,迭代器快速而干净地失败,而不是冒险,在未来不确定时间内的任意的、非确定性的行为。
迭代器的故障快速行为不能得到保证,事实上,一般来说,不可能作出任何硬的保证,存在不同步的并发修改。故障快速迭代器在尽力的基础上抛出ConcurrentModificationException、
*因此,编写一个依赖于此异常正确性的程序是错误的。迭代器的fail-fast行为只用于检测缺陷。
参考文档:
https://tech.meituan.com/java-hashmap.html
https://blog.csdn.net/liyantianmin/article/details/79401854