Map(1)


一:Map的概念

Map是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同;Map在java里是一个接口,常用的实现类有HashMap,LinkedHashMap,TreeMap和ConcurrentHashMap。 ①:在java中,哈希表的实现有数组+链表所组成。 ②:HashMap的底层数据结构实现由数组+链表/红黑树。 ③:LinkedHashMap底层数据结构实现由数组+链表+双向链表。 ④:TreeMap底层数据结构实现是由红黑树。 ⑤:ConcurrentHashMap底层数据结构实现由数组+链表/红黑树。   二:HashMap Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部(指到同一位置)也是通过单链表解决冲突问题。当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。

  图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

①:为什么要进行扩容

由于哈希是一种压缩映射,换句话说就是每一个Entry节点无法对应到一个只属于自己的桶(桶指的是数组中的位置),那么必然会存在多个Entry共用一个桶,拉成一条链表的情况,这种情况叫做哈希冲突。当哈希冲突产生严重的情况,某一个桶后面挂着的链表就会特别长,我们知道查找最怕看见的就是顺序查找,那几乎就是无脑查找。

哈希冲突无法完全避免,因此为了提高HashMap的性能,HashMap不得尽量缓解哈希冲突以缩短每个桶的外挂链表长度。

频繁产生哈希冲突最重要的原因就像是要存储的Entry太多,而桶不够,这和供不应求的矛盾类似。因此,当HashMap中的存储的Entry较多的时候,我们就要考虑增加桶的数量,这样对于后续要存储的Entry来讲,就会大大缓解哈希冲突。

因此就涉及到HashMap的扩容。
②:扩容

默认hashmap中默认的数组大小是多少,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

③:处理hash冲突

hash冲突就是根据key即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经有人先来了。就是说这个地方被抢了啦。这就是所谓的hash冲突啦。

我们用的是链地址法:这种方法的基本思想是数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中链地址法适用于经常进行插入和删除的情况。

④:hashmap转为红黑树的俩个条件

一个是链表长度到8,一个是数组长度到64.

当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。所以在1.8的时候进行了转为红黑树。

而JDK1.8中,HashMap采用数组+链表/红黑树(一种平衡搜索二叉树)实现,当链表长度超过阈值(8)时,且数组长度到了64,将链表转换为红黑树,这样大大减少了查找时间。 ⑤:hashmap的遍历
 Iterator> entryIterator = map.entrySet().iterator();
         while (entryIterator.hasNext()) {
            Map.Entry next = entryIterator.next();
            System.out.println("key=" + next.getKey() + " value=" + next.getValue());        }

Iterator iterator = map.keySet().iterator();
        while (iterator.hasNext()){
            String key = iterator.next();
            System.out.println("key=" + key + " value=" + map.get(key));

}

上面的俩种遍历方法,建议选用第一种,第一种我们将key和value同时取出来。第二分钟我们通过取出来key值后再进行取value。

  三:LinkedHashMap HashMap是一种非常常见、非常有用的集合,但在多线程情况下使用不当会有线程安全问题(hashmap和linkedhashmap都是线程不安全的)。

大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,但是hashmap也是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。

这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序。

特点总结:

1、LinkedHashMap的结构是数组+链表/红黑树+双向链表。双向链表是用来维护元素的顺序的。HashMap的元素是无序的,LinkedHashMap的元素是有序的。LinkedHashMap的存取数据的方式还是跟HashMap一致。

2、和HashMap的不同之处有:遍历方式不一样(扩容是用到遍历),HashMap根据按数组顺序来遍历,如果遇到链表,则依次遍历链表中的元素,完了之后继续遍历数组的元素。而LinkedHashMap是按双向列表来遍历的,所以LinkedHashMap遍历的性能比HashMap高一点,因为HashMap中有空元素。


3、LinkedHashMap中双向链表的顺序是按元素添加的顺序的,新添加的元素被加到双向链表的末尾。

4、LinkedHashMap和HashMap都是线程不安全的。

LinkedHashMap,总的来说,LinkedHashMap底层是使用HashMap+LinkedList实现的。用HashMap维护数据,用LinkedList来维护数据插入的顺序。

LinkedHashMap操作数据结构(比如put一个数据),和HashMap操作数据的方法完全一样,无非就是细节上有一些的不同罢了。

四:TreeMap

TreeMap:实现SortedMap接口(是Map的子接口),可以对key自动排序。存储结构也是红黑树。 TreeMap与LinkedHashMap的区别是:TreeMap对key自动排序,而LinkedHashMap按双向链表的顺序,双向链表的顺序是按元素添加的顺序。

TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。红黑树规则特点:

  1. 节点分为红色或者黑色;
  2. 根节点必为黑色;
  3. 叶子节点都为黑色,且为null;
  4. 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  5. 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  6. 新加入到红黑树的节点为红色节点; 我们的TreeMap是可以自动排序的,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。 HashMap 非线程安全 TreeMap 非线程安全 LinkedHashMap非线程安全
    1. HashMap无序,TreeMap固定的顺序,LinkedHashMap有序的(可以是插入顺序,也可访问顺序)