Java集合之HashMap/hashTable详解
作者:X-TIE
HashMap/hashTable详解
Map是映射键值的对象。map不能包含重复键:每个键最多只能映射一个值。它模拟了数学函数的抽象。
Map接口包括基本操作的方法(如put、get、remove、containsKey、containsValue、size和empty)、批量操作(如putAll和clear)和集合视图(如keySet、entrySet和values)。Java平台包含三个通用的映射实现:HashMap、TreeMap和LinkedHashMap。
它们的行为和性能与Set接口部分中描述的HashSet、TreeSet和LinkedHashSet类似。
下面从HashMap和hashTable两个容器分别介绍对比介绍一下。
下面我来分别看一下HashMap 和 hashTable 在无参构造函数实例化的具体实例:
由上图我们可以看到HashMap的无参数构造函数new 了一个:容量为16,加载因子为0.75,阈值为12的容器。
而hashTable的无参数构造函数则new 了一个:容量为11,加载因子为0.75,阈值为8的容器。
其中阈值为容量和加载因子的乘积,意思是如果容器到了这个值,那么就要实施扩容的机制了。下面我们看一下这两个容器到了阈值分别是如何扩容的呢?
首先是HashMap的扩容机制:
从源码上看,容器扩大了原容器的length*2倍。必须是2的冥。(2的几次方)
里面还有一个判断如果原来容器的容量已经达到了最大值,那么就把阈值调整到最大值,然后把原数组数据映射到新的更大的数组当中。这也就是说当数据量过多并且知道最大值的时候为了避免哈希表被重新散列(防止内部数据结构频繁被重新构建)。
然后我们看一下hashTable是如何扩容的:
原容器的大小乘以2+1,保证得到的数据是一个奇数。那么到这了我们考虑一下为什么table扩容要求是奇数,而map扩容必须是2的冥呢?
那么我们下面说一下HashMap的扩容机制以及确认元素位置的源码,来分析一下为什么设计成2的冥:
通过位运算符保证初始容量一定是2的冥
为了防止hash碰撞,在Entry数组(单链表)中为了保证每一个位置只有一个元素,通过hash%table.length=bucketIndex,bucketIndex为元素具体的位置,这样能够均匀的分布到容器的各个位置且不会有重复的。对集合操作效率也高。那么我们看一下源码中是如何找到元素具体的位置的:
为了减少碰撞HashMap是做了二次hash运算的。
h为最后计算的hash值,length为容器的容量。假设容量 = 16,我们计算一个hash值,来看一下h具体值为,并且我们计算一下indexFor的值是什么:
可以看到具体的hash值和在容器中的一个位置信息。
然后我们看一下,巧合的是根据我们的计算h & (length - 1) == h % length两个等式正好相等。且
这个的位运算的效率更高,这个应该就是容量必须为2的幂的原因。保证了数据分散的均匀。
并且通过二次hash减少碰撞,那么什么是碰撞呢?碰撞就是两个数计算出来的hash值一样,且equals e1.equals(e2)不相等,这样在一个hash位置上就会存储多个链表。在取值或者删除数据元素的时候效率比较低。
上面就是说的HashMap的容量为什么是2的冥的原因,下面来介绍一下hashTable的初始容量为什么是11,以及扩容机制?
hashTable的key获取hash为直接返回的当前key的hashCode值例如:如果是一个String的lisi返回3322014。
通过拆分lisi为char数组元素,且每个值拿到ASCII值的十进制。31*hash + 当前码值。
直接计算当前hash & long int的最大值%当前容器的容量,获得具体在容器中的位置。
int newCapacity = (oldCapacity << 1) + 1;这个是hashTable的一个扩容计算规则:保证了扩容后容量始终为奇数。
那么hashTable的扩容容量始终保证为奇数呢?
首先我猜测跟他的确认地址是有关系的,在就是由于hashTable全程加了同步锁为线程安全的,为了性能更高的操作容器才会这么设置,这块如果有小伙伴能讲解的比较清楚也欢迎评论交流指导。
最后总结一下:哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,所以单从这一点上看,HashTable的哈希表大小选择,似乎更高明些。但另一方面我们又知道,在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。所以从hash计算的效率上,又是HashMap更胜一筹。之所以不一样是因为HashMap用的位移运算确认具体位置,而hashTable是直接用的模。(事实就是HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改动。HashMap和HashTable在计算hash时都用到了一个叫hashSeed的变量。这是因为映射到同一个hash桶内的Entry对象,是以链表的形式存在的,而链表的查询效率比较低,所以HashMap/HashTable的效率对哈希冲突非常敏感,所以可以额外开启一个可选hash(hashSeed),从而减少哈希冲突。)
在结尾总结性的补充一下这个hashTable和HashMap的异同:
1.首先父类不同。hashTable的父类是Dictionary<K,V>,HashMap的父类是AbstractMap<K,V>.
2.HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。
3.初始化大小不同,扩容机制不同。
4.hashTable为线程安全的,方法级别的强制同步。HashMap非线程安全的。所以HashMap效率性能要高。
相同点:都实现了Map<K,V>接口。
到此这篇关于Java集合之HashMap/hashTable详解的文章就介绍到这了,更多相关HashMap/hashTable详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!