HashMap、LinkedHashMap、TreeMap、SortedMap、ConcurrentHashMap之间的区别

java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类,分别是HashMap Hashtable LinkedHashMap 和TreeMap.

Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。

Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
Map m=Collections.synchronizedMap(hashMap);//这样就可以让hashmap同步了

Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。

LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

一般情况下,我们用的最多的是HashMap,在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.

①HashMap的相关知识点
(1)HashMap的底层实现
在jdk1.8之前,hashmap底层是数组加链表的方式,hashmap通过扰动函数对key进行hashcode()计算,得到hash值,然后通过(n-1)&hash判断当前元素存放的位置,这里的n是指数组的长度,如果当前位置存在元素的话,就判断两者的hash值以及key值是否相同,如果相同,直接覆盖,如果不相同,就使用拉链法解决冲突。
这里的扰动函数指的是hashmap的hash方法,使用扰动函数就是为了防止一些实现比较差的hashcode(),导致频繁出现hash冲突。使用的目的就是减少冲突。
在1.8之后,就是用数组+链表+红黑树的方式存储,如果链表长度大于8时,就将链表转成红黑树,如果链表长度小于6时,就将红黑树转成链表。
为什么链表与红黑树是在6和8之间转换,那么7去哪里?
这是为了避免两种形式之间频繁的进行转换
如果hashmap的key是一个自定义的类,那么就要重写hashcode()和equals()
(2)HashMap什么时候开始扩容,怎么扩容的
当容器中元素个数>阈(yu)值时,就需要进行自动扩容(resize)了。
阈值=当前数组的长度 * 负载因子
hashmap的初始容量是16,负载因子是0.75,所以默认情况下,0.75 *16=12时,触发扩容机制:就是将当前容量扩大到当前容量的两倍。

1.7 中整个扩容过程就是一个取出数组元素(实际数组索引位置上的每个元素是每个独立单向链表的头部,也就是发生 Hash 冲突后最后放入的冲突元素)然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标然后进行交换(即原来 hash 冲突的单向链表尾部变成了扩容后单向链表的头部)。

而在 JDK 1.8 中 HashMap 的扩容操作就显得更加的骚气了,由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值与左移动的一位(newtable 的值)按位与操作是 0 或 1 就行,0 的话索引就不变,1 的话索引变成原索引加上扩容前数组,所以其实现如下流程图所示:

(3)为什么负载因子是0.75
在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素的个数和概率的对照表。
从上表可以看出当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为负载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
hash容器指定初始容量尽量为2的幂次方。
HashMap负载因子为0.75是空间和时间成本的一种折中。

②HashTable的相关知识点
(1)底层实现
HashTable的底层和hashmap一样,都是使用数组+单向链表,但是hashtable使用了synchronize保证了线程的安全性
(2)怎么保证安全性的
使用synchronize锁住了整个hashtable,所以效率很慢,所有访问hashtable的线程都要竞争同一把锁。

③ConcurrentHashMap的相关知识点
(1)底层实现
采用segment分段锁的方式。底层也是通过数组+链表的方式,将数组进行分段加锁处理,这样里提高了效率,同时保证了线程的安全性。
ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。

2)怎么保证安全性的
Concurrenthashmap线程安全的,1.7是在jdk1.7中采用Segment + HashEntry的方式进行实现的,lock加在Segment上面。1.7size计算是先采用不加锁的方式,连续计算元素的个数,最多计算3次:1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数;