Map是由一对对的key-value组成的,key要求是唯一的,value不要求。
通过看源码可以得出:key自带去重功能是Set类型的,value是Collection接口可存放任意集合。
来看一下Map的实现类:
HashMap、HashTable、ConcurrentHashMap之间的区别?
HashMap
JDK8之前HashMap是由数组+链表组成的,数组查找快增删慢,链表增删快查找慢,HashMap结合了两者的优势,HashMap不是线程安全的效率很高,组织键位如图:
没有给HashMap初始长度的时候HashMap默认初始长度是16,初始长度16的数组中每个数组的位置存放的是链表的头结点,可以通过模运算得到头结点的存放位置:hash(key.hashCode())%len,hashCode的运算本身是通过位运算得到的。
但是存在一种特殊情况,通过模运算得到的位置每次都是同一个这样的话就不断在一条链表中去插入,最坏的情况是时间复杂度从O(1)变成O(n)。
JDK8之后对HashMap进行了优化,将原先的HashMap由数组+链表组成的道理变成了数组+链表+红黑树。
添加红黑树之后再次遇到特殊的情况时就可以使用TREEIFY_THRESHOLD去判断是否将链表转换成一颗红黑树,这种情况下最坏的时间复杂度从O(n)变成O(nlogn)。
下面来看一下源码:
首先是HashMap的结点结构:
可以看出是一个数组,数组中的元素是一个个的头结点,看一下Node的内部构造:
hash是用来寻址的,next是用来连接下一个链表或者树的结点的。
接下来看一下从链表变成红黑树的边界:
当长度超过8链表会从链表变成红黑树,并且有TREEIFY_THRESHOLD,使链表变成红黑树来提高性能,如果删除结点,让出结点也是必然存在策略使红黑树变成链表的边界的。
就是UNTREEIFY_THRESHOLD等于6的时候,从红黑树变成链表。
看过HashMap的成员变量之后,再来看一下HashMap的构造函数:
通过loadFactor可以判断出来HashMap是懒加载的,就是说当创建的时候不会去分配加载空间,只有在使用的时候才会去初始化和Hibernate的延迟加载是一样的。
当put时候才是初始化的时候,下面来看看一下put的方法:
put实际上使用的是putVal方法,看看其方法的实现逻辑:
当第一次使用时候总和超过16的时候,HashMap会去分配合适的空间去使用,并且在里面存在添加之后是否树化,分配寻址空间等等细节的逻辑。
对HashMap的put方法简单总结一下:
- 如果HashMap没有被初始化,则初始化;
- 对key求hash值,然后再计算下标;
- 如果没有碰撞,直接放入桶中;
- 如果碰撞以链表的方式链接到后面;
- 如果链表长度超过阈值,把链表转换成红黑树;
- 如果链表长度低于6,将红黑树转换成链表;
- 如果节点已经存在,就替换旧值;
- 如果桶满了(容量16 * 0.6),需要resize扩容二倍之后重排。
再来看一下HashMap的get方法的实现逻辑:
主要是实现逻辑是使用hash算法找到对应的bucket桶,然后用key.equals找到地址空间,最后找到值返回。
除了树化这种方式被动的提升性能之外hash运算也是可以提升性能的,但是hash运算是存在哈希碰撞的下面是减少hash碰撞概率的方法:
- 扰动函数:促使元素位置分布均匀,减少碰撞几率;
- 使用final对象:采用合适的equals()和hashCode()方法。
HashMap扩容的问题:
- 多线程情况下,调整大小存在条件竞争,容易发生死锁;
- rehashing是一个比较耗时的过程。
来源:CSDN
作者:唉.
链接:https://blog.csdn.net/weixin_44240370/article/details/104137551