HashMap、LinkedHashMap、ConcurrentHashMap、ArrayMap、SparseMap、Hashset、TreeMap的底层原理、数据结构与对比
1、HashMap
底层数据结构(数组+链表+红黑树),链表中节点数量大于8则转换为红黑树。
参数对性能的影响:加载因子(loadFactor)和初始容量(initialCapacity)。加载因子跟初始容量得乘积是判断是否需要扩容的依据,当节点个数大于乘积则扩容。如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值(降低后减少碰撞,时间复杂度可以看作O(1));相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1(减小数组的大小,增大碰撞几率)。
hash 函数的意义:
//节点key的hash值计算方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//根据hash值计算放在哪个桶中
p = tab[i = (n - 1) & hash])
32位hash码右移16位,异或,再与数组长度减1的值做按为与计算,保留后几位的值。右移16位再异或是为了保留高位信息,增大随机性,数组长度减一说明了为什么容量必须是2的倍数。
https://www.zhihu.com/question/20733617
扩容过程:容量变了原来的2倍,同时根据e.hash & oldCap等于0还是1判断是否需要移动。(假设容量16(10000),那么同一个桶中所有节点后4位肯定相同,右数第五位不一定相同),为0则在原来位置,为1则把节点放到原来位置加上扩容容量(原来5,现在是5+16 = 21)。
https://www.cnblogs.com/hongdada/p/6024832.html
线程不安全体现在哪?
1、相同的hash执行插入put()操作可能导致数据被覆盖
2、JDK1.8以前resize()扩容时,是头插入,有可能形成环形链表,导致死循环;JDK1.8以后扩容时是尾插入,不会出现这种情况。
https://blog.csdn.net/v123411739/article/details/78996181
2、LinkedHashMap
底层数据结构:LinkedHashMap继承自HashMap,底层数据结构跟HashMap一样,但是其节点继承HashMap中的Node外,还增加2个指针,before和after,当执行put后,会把after之下新增加的节点,所以LinkedHashMap也可以当作是一个链表。
特点:LinkedHashMap中的链表可以按照插入顺序排序,也可以按照访问顺序排序。当通过get()操作访问某个节点后,会把该节点放到链表的尾部,这也是Lrucache的基础。
使用场景: Lrucache
Lrucache : least recent used,最近最少使用原则。底层使用LinkedHashMap控制缓存在一定的内存容量内。执行put操作时先检查是否超过规定的容量,超过则把最近最少使用的缓存去掉。根据LinkedHashMap按照访问顺序排序的原理,最近访问的元素放在链表尾部,也就是LinkedHashMap最前面的节点替换成新的缓存。
3、ConcurrentHashMap
相对与HashTable在所有方法加锁来确保线程安全,ConcurrentHashMap做了以下优化:通过CAS和锁住桶确保线程安全。CAS避免引起线程切换,消耗cpu资源;而锁住桶相比锁住整个HasMap减小了锁的粒度。(并发put的的时候可能会导致被覆盖,而锁住桶能避免这个问题)
4、ArrayMap、SparseMap
ArrayMap设计上更多考虑的是内存优化,内部使用2个数组进行存储,一个记录key的hash值,另外一个记录key和value,相比hashMap的节点node而言,少了next这个成员变量;而对于数据的插入和获取,采用的二分查找,没有hash算法快,插入时还涉及到数组的搬移,整体的时间效率不如hashMap。
sparseArray的key是int类型,根据key来确定位置,相比HashMap少了key的hash值和next指针;put和add操作同样是基于2分查找。
https://www.jianshu.com/p/7b9a1b386265
http://gityuan.com/2019/01/13/arraymap/
5、HashSet
HashSet 是一个没有重复元素的集合。
它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素。
Set的元素是HashMap的key,value是一个固定的object。通过hash算法实现元素的不可重复以及快速查找。
private transient HashMap map;
private static final Object PRESENT = new Object();
//构造方法
public HashSet() {
map = new HashMap<>();
}
//add
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
6、TreeMap
主要理解红黑树的2个方面:
红黑树的优势和特性
红黑树什么时候变色,什么时候旋转。
红黑树是一种二叉查找树,由于二叉查找树在插入数据时有可能失衡,变成链表,最坏时间复杂度O(n),通过构造红黑树使得二叉树平衡,最坏时间复杂度为O(logn)。
红黑树有5个特性:
1、每个节点要么是黑色,要么是红色。
2、根节点为黑色。
3、所有叶子节点(nil)为黑色。
4、父子节点不能同时为红色。
5、节点到叶子节点各条路径上黑节点的数量是一样的。
插入节点的时候:
1、默认插入的节点是红色的,避免破坏特性5。
2、插入节点的父节点为黑色时,符合红黑树特性,不做改变。
3、插入节点的父节点为红色时,分为3种情况:
a、父节点为红色,叔叔节点为红色,此时肯定有祖父节点,且祖父节点为黑色,此时需要将父节点和叔叔节点变为红色,祖父节点变为黑色。为什么要这样做?只将父节点变为黑色行不行?不可以,会导致父节点这条路径的黑色节点比叔叔节点路径的黑节点多一,破坏特性5。通过这样子改变能确保符合特性5。
涂色.png
b、父节点为红色,叔叔节点为黑色,且当前节点为父节点的右节点,此时需要将父节点作为当前节点,进行左旋。
们处理红黑树的核心思想:将红色的节点移到根节点;然后,将根节点设为黑色。既然是“将红色的节点移到根节点”,那就是说要不断的将破坏红黑树特性的红色节点上移(即向根方向移动)。 而S又是一个右孩子,因此,我们可以通过“左旋”来将S上移!
左旋.png
c、父节点为红色,叔叔节点为黑色,且当前节点为父节点的左节点,此时需要将父节点改为黑色,祖父节点改为红色,同时以祖父节点为当前节点,进行右旋。
右旋.png