HashMap、HashTable区别。

1、HashMap线程不安全,HashTable线程安全;

2、HashMap的键和值都允许null值存在,而HashTable不允许;

3、HashMap的效率高于Hashtable

 

* Hash table based implementation of the <tt>Map</tt> interface.  This
* implementation provides all of the optional map operations, and permits
* <tt>null</tt> values and the <tt>null</tt> key.  (The <tt>HashMap</tt>
* class is roughly equivalent to <tt>Hashtable</tt>, except that it is
* unsynchronized and permits nulls.)  This class makes no guarantees as to
* the order of the map; in particular, it does not guarantee that the order
* will remain constant over time.

 

HashMap是一哈种希表(链表的数组)的结构存储。HashMap的底层主要是基于数组和链表来实现的,通过计算散列来决定存储的位置(数组的下标),数组的每一个元素都是一个单链表的头节点。HashMap通过key的hashCode来计算hash值,hash值相同的对象在数组中的位置一样,此时出现hash冲突,HashMap通过链表的方式解决。

 

使用对象作为HashMap和HashTable的key时,需要去实现对象的hashCode()方法和equals()方法。

 

决定HashMap性能的2个关键因素:

initial capacity(初始化容量,默认16)和 load factor(加载因子,填满的程度,默认是0.75)。初始化容量必须为2的n次方。加载因子越大,填满的元素越多,空间利用率越高,hash冲突机会越大,链表长度越长,查找效率越低;加载因子越小,填满的元素越少,数据会过于疏散,空间会浪费(很多空间还没用就可能需要扩容),但是hash冲突会减少。

 

HashMap计算散列(数组下标)的公式:(table.length-1)& hash

HashTable计算散列的公式:(hash & 0x7FFFFFFF) % tab.length(除法散列法)

相比较而言,取模会使用除法效率偏低,HashMap比HashTable效率高的一点表现。因为HashMap计算是使用(table.length-1)& hash。如果length为奇数,则(table.length-1)为偶数,最后一位(二进制)为0,这样通过&计算导致得出的散列值最后一位也为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了一半的空间,因此,length取2的整数次幂,即是为了使不同hash值发生碰撞的概率较小,也能避免过多的浪费空间使元素在哈希表中均匀地散列。

 

HashMap的扩容:newCap = oldCap << 1

当元素个数超过数组大小*load factor时,就会进行扩容,容量扩大一倍。即默认情况下,HashMap元素个数超过16*0.75=12时,数组大小扩大到2*16=32,然后需要重新计算每个元素在数组中的位置(length改变导致散列值变化),扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以能预知HashMap元素数量时,预设元素个数就能够有效的提高HashMap性能。

HashTable的扩容:int newCapacity = (oldCapacity << 1) + 1;

 

如何能让HashMap同步:Map m = Collections.synchronizeMap(hashMap);

 

注意

JDK1.8之前:使用单向链表来存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。

在JDK1.8:为了解决在频繁冲突时hashmap性能降低的问题,使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)。

在Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引一样时,HashMap会使用树来存储它们。

这一动态的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值时用平衡二叉树替换链表。不过这一特性在所有基于hash table的类中并没有,例如Hashtable和WeakHashMap。目前,只有ConcurrentHashMap,LinkedHashMap和HashMap会在频繁冲突的情况下使用平衡树。

 

在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”.

既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。