HashMap简介
JDK1.8后的HashMap在底层数据结构上采纳数组+链表/红黑树,通过散列映射来存储键值对数据,因为在查问上应用散列码hashcode,所以在查问上的访问速度较快。HashMap能够存储值为null的键(key)和值(value),然而null作为键只能有一个,而null作为值能够有多个。它是无序的、非线程平安的。
HashMap底层数据结构
在JDK1.7及之前的HashMap底层是由数组+链表形成,也就是链表散列。其中,数组是HashMap的主体,链表则是次要为了解决哈希抵触而存在的。
JDK1.8当前的HashMap在解决哈希抵触时有了较大的变动,当链表长度大于阈值(默认为8)(将链表转换为红黑树之前会判断,如果以后数组的长度小于64,那么会抉择先进行数组扩容,而不是间接转换为红黑树)时,将链表转化为红黑树,以缩小搜寻工夫。
HashMap底层实现
通过key的hashcode通过扰动函数解决后失去hash值,而后通过(n-1) & hash判断以后元素寄存的地位(这里的n指的是数组的长度),如果以后地位存在元素的话,就判断该元素与要存入的元素的hash值以及key是否雷同,如果雷同的话,间接笼罩;不雷同就通过拉链法解决抵触。
扰动函数:hashmap的hash办法,应用hash办法即扰动函数为了避免一些实现比拟差的hashcode()办法,换句话说扰动函数能够缩小碰撞。
拉链法:将链表和数组相结合。即创立一个链表数组,数组中每一格就是一个链表。若遇到哈希抵触,则将抵触的值加到链表中即可。
HashMap的长度为什么是2的幂次方
hash值的取值范畴是2147483648到2147483647,大略有40亿的映射空间,只有hash函数映射的比拟平均涣散,个别很难呈现碰撞。然而一个长度为40亿的数组,内存是放不下的。因而这个散列值不能间接拿来用,须要先对数组的长度进行取模运算,失去的余数用来作为寄存的地位也就是对应的数组下标。这个取模运算计算方法是“(n-1)&hash”(n代表数组长度)。
这个时候有人要问:取模操作不是%吗?
起因:采纳二进制位操作&,绝对于%可能进步运算效率。因而,取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作,也就是说,hash%length==hash&(length-1),而这个等式成立的前提是length是2的n次方。
HashMap的扩容机制
要理解HashMap的扩容过程,首先须要理解HashMap中的变量
Node<K,V>:链表节点,蕴含了key,value,hash值,next指针四个元素;table:Node<K,V>类型的数组,外面的元素是链表,用来寄存HashMap元素的实体;size:示意以后HashMap蕴含的键值对数量final float loadFactor:负载因子,用于扩容int threshold:扩容的阈值,决定了HashMap何时扩容,以及扩容后的大小,个别等于table * loadFactorstatic final float DEFAULT_LOAD_FACTORY = 0.75f:默认的负载因子static final int DEFAULT_INITIAL_CAPACITY = 1<<4:默认的table初始容量(16)static final int TREEIFY_THRESHOLD = 8:链表长度大于该参数转红黑树statci final int UNTREEIFY_THRESHOLD = 6:当树的节点数小于等于该参数转成链表
何时进行扩容?
当以后存入的数据容量大于阈值threshold时须要进行resize扩容。而threshold取决于两个因素:以后长度容量capacity以及负载因子loadFactory。
threshold = capacity * loadFactory
HashMap扩容能够分为三种状况
第一种:应用默认构造方法初始化HashMap。HashMap在一开始初始化的时候会返回一个空的table,并且threshold为0。因而第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY = 1<<4,因而threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。
第二种:指定初始容量的办法结构HashMap。初始容量等于threshold,接着threshold = 以后的容量(threshold) * DEFAULT_LOAD_FACTOR。
第三种:HashMap不是第一次扩容。如果HashMap曾经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。
上面是HashMap扩容机制外围办法Node<K,V>[] resize()的源码
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//首次初始化后的table为null int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold;//默认结构器下的阈值为0 int newCap, newThr = 0; if (oldCap > 0) { //table扩容过 //以后table容量大于最大值的时候返回以后table if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //table的容量乘以2,threshold的值也乘以2 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold //应用带有初始容量的结构器时,table容量为初始化失去的threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //应用带有初始容量的结构器在此处进行扩容 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) //以后index没有产生hash抵触,间接对2取模,即移位运算hash & (2^n-1) //扩容都是依照2的幂次方进行扩容,因而newCap = 2^n newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //以后index对应的节点为红黑树,当树的个数小于等于UNTREEIFY_THRESHOLD则转成链表 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order 把以后index对应的链表分成两个链表,缩小扩容的迁徙量 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { //扩容后不须要挪动的链表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { //扩容后须要挪动的链表 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; //扩容长度为以后index地位+旧的容量 newTab[j + oldCap] = hiHead; } } } } } return newTab; }}
HashMap与HashSet、HashTable的区别
HashMap和HashSet
HashSet底层是基于HashMap实现的,且都是非线程平安的
HashMap和HashTable
ConcurrentHashMap
JDK1.7
由Segment数组+HashEntry数组+链表实现:
ConcurrentHashMap分段锁对整个桶数组进行了宰割分段(segment),每一把锁只锁容器的其中一部分数据,多线程拜访容器里不同数据段的数据,就不会存在锁竞争,进步并发访问率。
JDK1.8
到了JDK1.8的时候曾经摒弃了segment的概念,而是间接用Node 数组+链表+红黑树实现。同时采纳CAS 和 synchronized 来保障并发平安,整个看起来就像是优化且线程平安的HashMap。
synchronized 只锁定以后链表或红黑二叉树的首节点,这样只有 hash 不抵触,就不会产生并发,效率又晋升 N 倍。
汇合框架底层数据结构总结
那么如何选用汇合呢?
次要依据汇合的特点来选用,比方咱们须要依据键值获取到元素值时就选用 Map 接口下的汇合,须要排序时抉择 TreeMap ,不须要排序时就抉择 HashMap ,须要保障线程平安就选用ConcurrentHashMap 。
当咱们只须要寄存元素值时,就抉择实现 Collection 接口的汇合,须要保障元素惟一时抉择实现 Set 接比的汇合比方 TreeSet 或 HashSet ,不须要就抉择实现 List 接口的比方 ArrayList 或 LinkedList ,而后再依据实现这些接口的汇合的特点来选用。
最初
在文章的最初作者为大家整顿了很多材料!包含java外围知识点+全套架构师学习材料和视频+一线大厂面试宝典+面试简历模板+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书等等!有须要的敌人能够关注公众号:前程有光回复材料主动下载