Map常用实现类总结
Map是一种(key/value)的映射结构,其它语言里可能称作字典(Dictionary),包括java早期也是叫做字典,Map中的元素是一个key只能对应一个value,不能存在重复的key。
java中提供的Map的实现主要有HashMap、LinkedHashMap、WeakHashMap、TreeMap、ConcurrentHashMap、ConcurrentSkipListMap,另外还有两个比较古老的Map实现HashTable和Properties。
HashMap
HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改的速度都很快,能达到O(1)的平均时间复杂度。它是非线程安全的,且不保证元素存储的顺序;
参考博客:【死磕 Java 集合】— HashMap源码分析
在Java中,HashMap的实现采用了(数组 + 链表 + 红黑树)的复杂结构,数组的一个元素又称作桶。
当一个链表的元素个数达到一定的数量(8)(且数组的长度达到一定的长度(64))后,则把链表转化为红黑树,从而提高效率。当红黑树个数小于6时 ,退化为链表。
数组的查询效率为O(1),链表的查询效率是O(k),红黑树的查询效率是O(log k),k为桶中的元素个数,所以当元素数量非常多的时候,转化为红黑树能极大地提高效率。
(1)容量
容量为数组的长度,亦即桶的个数,默认为16,最大为2的30次方,当容量达到64时才可以树化。
(2)装载因子
装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
(3)树化
树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。
(4) 构造方法
HashMap(int initialCapacity, float loadFactor)
初始化大小取的是最近的2的n次方:initialCapacity为18就是32的hashMap初始大小,取向上最近的2的n次方
决定是在tableSizeFor方法
comparableClassFor方法解析
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) { // 判断是否实现了 Comparable接口 只要在继承链上有这个类型就可以了
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class)
return c; // 如果是String类型,直接返回String.class
if ((ts = c.getGenericInterfaces()) != null) { // 判断是否有直接实现的接口 自己直接实现的才行
for (int i = 0; i < ts.length; ++i) { // 遍历直接实现的接口
if (((t = ts[i]) instanceof ParameterizedType) && // ParameterizedType 该接口实现了泛型
((p = (ParameterizedType)t).getRawType() == //getRawType 获取接口不带参数部分的类型对象
Comparable.class) && // 该类型是Comparable
(as = p.getActualTypeArguments()) != null && //getActualTypeArguments 获取泛型参数数组
as.length == 1 && as[0] == c) // 只有一个泛型参数,且该实现类型是该类型本身
return c; // 返回该类型
}
}
}
return null;
}
参考博客: HashMap.comparableClassFor(Object x)方法解读
Java 8 HashMap键与Comparable接口
Java 8 做了很多优化,其中也包括HashMap 类。在 Java 7 中,两个不同的元素,如果它们的键产生了冲突,那么会被存储在同一个链表中。而从 Java 8 开始,如果发生冲突的频率大于某一个阈值(8),并且 map 的容量超过了另一个阈值(64),整个链表就会被转换成一个二叉树。
原来如此!所以,对于没有实现 Comparable 的键,最终的树是不平衡的;而对于实现了 Comparable 的键,其二叉树就会是高度平衡的。事实是这样吗?不是。HashMap 内部是红黑树,也就是说它总是平衡的。我通过反射机制,查看了最终的树结构。对于拥有 50000 个元素(不敢让数字更大了)的 HashMap 来说,两种不同的情况下(实现或是不实现 Comparable 接口)树的高度都是 19 。
那么,为什么之前的实验结果会有那么大的差别呢?原因在于,当 HashMap 想要为一个键找到对应的位置时,它会首先检查新键和当前检索到的键之间是否可以比较(也就是实现了 Comparable 接口)。如果不能比较,它就会通过调用 tieBreakOrder(Object a,Object b) 方法来对它们进行比较。这个方法首先会比较两个键对象的类名,如果相等再调用 System.identityHashCode 方法进行比较。这整个过程对于我们要插入的 500000 个元素来说是很耗时的。另一种情况是,如果键对象是可比较的,整个流程就会简化很多。因为键对象自身定义了如何与其它键对象进行比较,就没有必要再调用其他的方法,所以整个插入或查找的过程就会快很多。值得一提的是,在两个可比的键相等时(compareTo 方法返回 0)的情况下,仍然会调用 tieBreakOrder 方法。
总而言之,在 Java 8 的 HashMap 里,如果一个桶里存放了大量的元素,它在达到阈值时就会被转换为一棵红黑树,对于实现了 Comparable 接口的键来说,插入或删除的操作会比没有实现 Comparable 接口的键更简单。通常,如果一个桶不会发生那么多次冲突的话,这整个机制不会带来多大的性能提升,但起码现在我们对 HashMap 的内部原理有了更多了解。
参考博客:Java 8 HashMap键与Comparable接口
红黑树
红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树。
红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。
除了具备该特性之外,红黑树还包括许多额外的信息。
红黑树的每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black)。
红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
关于它的特性,需要注意的是:
第一,特性(3)中的叶子节点,是只为空(NIL或null)的节点。
第二,特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
总结
(1)HashMap是一种散列表,采用(数组 + 链表 + 红黑树)的存储结构;
(2)HashMap的默认初始容量为16(1<<4),默认装载因子为0.75f,容量总是2的n次方;
(3)HashMap扩容时每次容量变为原来的两倍;
(4)当桶的数量小于64时不会进行树化,只会扩容;
(5)当桶的数量大于64且单个桶中元素的数量大于8时,进行树化;
(6)当单个桶中元素数量小于6时,进行反树化;
(7)HashMap是非线程安全的容器;
(8)HashMap查找添加元素的时间复杂度都为O(1);
LinkedHashMap
参考博客: 【死磕 Java 集合】— LinkedHashMap源码分析
LinkedHashMap内部维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问顺序访问,可以用来实现LRU缓存策略。
LinkedHashMap可以看成是 LinkedList + HashMap。
我们知道HashMap使用(数组 + 单链表 + 红黑树)的存储结构,那LinkedHashMap是怎么存储的呢?
通过上面的继承体系,我们知道它继承了Map,所以它的内部也有这三种结构,但是它还额外添加了一种“双向链表”的结构存储所有元素的顺序。
添加删除元素的时候需要同时维护在HashMap中的存储,也要维护在LinkedList中的存储,所以性能上来说会比HashMap稍慢
属性
(1)head
双向链表的头节点,旧数据存在头节点。
(2)tail
双向链表的尾节点,新数据存在尾节点。
(3)accessOrder
是否需要按访问顺序排序,如果为false则按插入顺序存储元素,如果是true则按访问顺序存储元素(实现LRU缓存策略的关 键)。
afterNodeAccess(Node<K,V> e)方法
在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrder为true,调用这个方法把访问到的节 点 移动到双向链表的末尾
参考博客 : 通过分析LinkedHashMap了解LRU
总结
(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;
(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;
(3)如果accessOrder为false,则可以按插入元素的顺序遍历元素;
(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;
(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;
(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;
(7)LinkedHashMap可以用来实现LRU缓存淘汰策略;
TreeMap
关于Map的问题主要有:
(1)什么是散列表?
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
(2)怎么实现一个散列表?
(3)java中HashMap实现方式的演进?
(4)HashMap的容量有什么特点?
默认情况下,当我们设置HashMap的初始化容量时,实际上HashMap会采用第一个大于该数值的2的幂作为初始化容量。
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
通过几次无符号右移和按位或运算,我们把1100 1100 1100转换成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,这就是大于1100 1100 1100的第一个2的幂。 好了,我们现在解释清楚了Step 1和Step 2的代码。就是可以把一个数转化成第一个比他自身大的2的幂。(可以开始佩服Java的工程师们了,使用无符号右移和按位或运算大大提升了效率。)
基础:
所以,我可以认为,当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。
(5)HashMap是怎么进行扩容的?
(6)HashMap中的元素是否是有序的?
HashMap无序,linkedHashMap迭代时按照插入顺序迭代,TreeMap和TreeSet是有序的。
(7)HashMap何时进行树化?何时进行反树化?
如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,treeifyBin首先判断当前hashMap的长度,如果不足 64,只进行resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树。
当链表的元素大于8时进行树化,小于6时进行反树化。
(8)HashMap是怎么进行缩容的?
它不会动态地进行缩容,也就是说,你不应该保留一个已经删除过大量Entry的HashMap(如果不打算继续添加元素的话),此时它的buckets数组经过多次扩容已经变得非常大了,这会占用非常多的无用内存,这样做的好处是不用多次对数组进行扩容或缩容操作。不过一般也不会出现这种情况,如果遇见了,请毫不犹豫地丢掉它,或者把数据转移到一个新的HashMap。
(9)HashMap插入、删除、查询元素的时间复杂度各是多少?
HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可O(1);如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
(10)HashMap中的红黑树实现部分可以用其它数据结构代替吗?
(11)LinkedHashMap是怎么实现的?
具体参见文章 LinkedHashMap部分
(12)LinkedHashMap是有序的吗?怎么个有序法?
linkedHashMap迭代时按照插入顺序迭代
(13)LinkedHashMap如何实现LRU缓存淘汰策略?
LRU是最近最少使用,根据数据的历史访问记录来进行淘汰数据的。其核心思想是如果数据最近被访问过,那么将来访问的几率也更高。在这里提一下,Redis缓存和MyBatis二级缓存更新策略算法中就有LRU。画外音:LFU是频率最少使用,根据数据历史访问的频率来进行淘汰数据。其核心思想是如果数据过去被访问多次,那么将来访问的几率也更高。
LRU是通过双向链表来实现的。当某个位置的数据被命中,通过调整该数据的位置,将其移动至尾部。新插入的元素也是直接放入尾部(尾插法)。这样一来,最近被命中的元素就向尾部移动,那么链表的头部就是最近最少使用的元素所在的位置。
HashMap的afterNodeAccess()、afterNodeInsertion()、afterNodeRemoval()方法都是空实现,留着LinkedHashMap去重写。LinkedHashMap靠重写这3个方法就完成了核心功能的实现。不得不感叹,HashMap和LinkedHashMap设计之妙。
https://www.jianshu.com/p/b8b00da28a49
(14)WeakHashMap使用的数据结构?
(15)WeakHashMap具有什么特性?
(16)WeakHashMap通常用来做什么?
(17)WeakHashMap使用String作为key是需要注意些什么?为什么?
(18)什么是弱引用?
Java中的弱引用具体指的是java.lang.ref.WeakReference<T>类,我们首先来看一下官方文档对它做的说明:
弱引用对象的存在不会阻止它所指向的对象被垃圾回收器回收。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的 全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它 随后就会被回收。与此同时或稍后,垃圾收集器会把那些刚清除的弱引用放入创建弱引用对象时所指定的引用队列(Reference Queue)中。
(19)红黑树具有哪些特性?
红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
关于它的特性,需要注意的是:
第一,特性(3)中的叶子节点,是只为空(NIL或null)的节点。
第二,特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
(20)TreeMap就有序的吗?怎么个有序法?
有序,这里有序指的是TreeMap中的键是有序的,我们可以知道Map中的键可以转化一个Set集合,所以实现TreeMap排序的方法是实现,TreeMap中键对象的排序;
排序方法当然也有两种:
第一种:TreeMap(Comparator<? super K> comparator) 传入一个比较器;
第二种:将 存储的键对象实现Comparable接口;
(21)TreeMap是否需要扩容?
因为是底层数据结构只有红黑树,不需要扩容
(22)什么是左旋?什么是右旋?
(23)红黑树怎么插入元素?
(24)红黑树怎么删除元素?
(25)为什么要进行平衡?
(26)如何实现红黑树的遍历?
(27)TreeMap中是怎么遍历的?
(28)TreeMap插入、删除、查询元素的时间复杂度各是多少?
log(n)
(29)HashMap在多线程环境中什么时候会出现问题?
(30)ConcurrentHashMap的存储结构?
(31)ConcurrentHashMap是怎么保证并发安全的?
(32)ConcurrentHashMap是怎么扩容的?
(33)ConcurrentHashMap的size()方法的实现知多少?
(34)ConcurrentHashMap是强一致性的吗?
(35)ConcurrentHashMap不能解决什么问题?
(36)ConcurrentHashMap中哪些地方运用到分段锁的思想?
(37)什么是伪共享?怎么避免伪共享?
(38)什么是跳表?
(40)ConcurrentSkipList是有序的吗?
(41)ConcurrentSkipList是如何保证线程安全的?
(42)ConcurrentSkipList插入、删除、查询元素的时间复杂度各是多少?
(43)ConcurrentSkipList的索引具有什么特性?
(44)为什么Redis选择使用跳表而不是红黑树来实现有序集合?