【干货预警,强烈建议关注收藏阅读】
【本篇是集合中的Map篇,以下涉及源码基于JDK1.8】
(为何上一篇介绍了Collection下的list,不接着介绍Collection下的set,因为Set集合实际上就是HashMap来构建的!没看过List篇的移步这里了)
本篇是Java基础中最重要的知识点集合Map篇。Java集合是java提供的工具包,包含了常用的数据结构:集合、链表、队列、栈、数组、映射等。Java集合工具包位置是java.util.*,Java集合主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)。接下来我们一起学习吧,博主水平有限,哪里有不对的地方欢迎大佬指出斧正。
Colletcion和Map结构如下图所示:
大致介绍一下:Collection和Map是两个高度抽象的接口
- Collection抽象的是集合,包含了集合的基本操作和属性,Collection主要包含List和Set两大分支。
- List是有序的链表,允许存储重复的元素,List的主要实现类有LinkedList, ArrayList, Vector, Stack。
- Set是不允许存在重复元素的集合,Set的主要实现类有HastSet和TreeSet(依赖哈希实现,后面介绍)。
- Map是一个映射接口,即存储Key-Value键值对的集合(和redis存储类似),AbstractMap是个抽象类,它实现了Map接口中的大部分API,而常见的HashMap,TreeMap都是继承于AbstractMap。HashTable继承于Dictionary,但也实现了Map接口。
集合是Java中用来存储多个对象的一个容器,我们知道容器数组,数组长度不可变,且只能存储同样类型的元素,数组可以存储基本类型或者引用类型;而集合长度可变,可以存储不同类型元素(但是我们一般不这么干),集合只能存储引用类型(存储的基本类型会变成包装类);
集合的Fail-Fast机制?
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。
例如:当某一个线程A通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException 异常,产生 fail-fast 事件;当然,不仅是多个线程,单个线程也会出现 fail-fast 机制,比如单线程下的iterator迭代器遍历时调用集合的增删改等操作会抛出java.util.ConcurrentModificationException,从而产生fail-fast机制。
HashMap
HashMap定义:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable { }
HashMap简介:
-
HashMap是基于Map接口的Key-Value的集合,允许使用null值和null键,但不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap在底层将Key-Value当成一个整体Entry来处理。
-
底层使用数组实现,数组中每一项是单向链表,即数组和链表的结合体;链表长度大于一定阈值时,链表转换为红黑树。后面详细分析源码介绍
-
在链表和数组中可以说是有序的(存储的顺序和取出的顺序是一致的),但同时也带来缺点:想要获取某个元素便要访问所有的元素直到找到为止(List中知道具体位置可以直接访问)。HashMap可以不在意元素的顺序,能够快速的查找定位到元素。
-
散列表HashMap会为每一个Key计算出哈希值,即散列码,根据这些散列码保存在对应的位置上。如下图所示的数组+链表实现,一个hash值会遇到被占用的情况(hashCode散列码相同,就存储在同一个位置上),这种情况是无法避免的,这种现象称之为:散列冲突。后面详细介绍冲突以及解决方法等
HashMap源码及重点关注问题:
- 属性和默认值:
注意:初始容量太高和负载因子太低的话,遍历效率不太好。 - 构造函数:
这里我们需要注意第一个构造函数,传入初始容量和负载因子,如果初始容量小于0抛出异常,大于最大容量MAXIMUM_CAPACITY则置为MAXIMUM_CAPACITY;如果不是这俩个会进入tableSizeFor()方法,一起来看看:
int n = cap - 1;
为什么要有第一步的减一操作?
目的是为了防止传入的cap已经是2的幂,如果没有执行这个减一操作,返回的将是这个cap(传入是2的幂)的2倍,不太懂?请继续读下去。
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
重点:如果传入的cap为0,经过减一和后面几次的无符号移动之后会一直是0,最后返回capacity是1;主要考虑cap不为0的情况,由于cap不为0,则n的二进制中总会有一个bit位为1,我们只考虑最高bit位为1。
第一步n >>> 1是无符号右移1位,再做或运算,使得二进制中的最高位和紧邻的右边一位也是1(0000 11## #### ####这种格式)。
第二步n >>> 2是无符号右移两位再做或操作,第一步已经将最高位和其临位变为1,这一步则会将从高位开始的4位都变成1(0000 1111 #### ####这种格式)。
第三步n >>> 4是无符号右移4位再做或操作,同样的道理,经过这一步之后从最高位开始后面的8位都变为1(0000 1111 1111 ####这种格式)。
以此类推,容量最大是32位正数,经过n |= n >>> 16之后最多也就32个1(1+2+4+8+16=31,但是别忘了开始的那一位),此时已经是负数了。
在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(2 ^ 30),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作。所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。30个1,加1之后得2 ^ 30) 。
举个例子:
现传入一个数cap为11,n=cap-1为10,则n二进制为0000 1010。
n |= n >>> 1:n >>> 1变为0000 0101,n或n>>> 1变为0000 1101,n即此值;
n |= n >>> 2:n >>> 2变为0000 0011,n或n>>> 2变为0000 1111,n即此值;
n |= n >>> 4:n >>> 4变为0000 0000,n或n>>> 4变为0000 1111,n不变;
n |= n >>> 8:n >>> 8变为0000 0000,n或n>>> 8变为0000 1111,n不变;
n |= n >>> 16:n >>> 16变为0000 0000,n或n>>> 16变为0000 1111,n不变;
看完上面可能会感到奇怪的是:为啥是将2的整数幂的数赋给threshold?
threshold这个成员变量是阈值,决定了是否要将散列表再散列。它的值应该是:capacity * load factor才对的。其实这里仅仅是一个初始化,当创建哈希表的时候,它会重新赋值的。
- 内部类Node:
- put()方法–放入(Key,Value)键值对:
内部包含hash()函数和putVal()函数,hash函数用来求哈希值,putVal()函数用来将key-value键值对放入集合map,我们先看hash()函数:
这里直接将key的哈希值返回不就好了, 为什么还要做异或运算?
我们进入putVal()函数看:
这里是根据key的哈希值来保存在散列表中的,刚刚上面的key原哈希值和高16位异或运算得到的新哈希值便是用来计算保存在散列表中位置的。
我们表的默认初始容量是16,要放到散列表中即为0-15的位置,即tab[i = (n - 1) & hash]这里。我们可以发现在做&运算的时候,仅仅是后4位有效(容量32则是后5位有效,依次类推),那如果我们key的哈希值高位变化很大,低位变化很小,直接拿过去做&运算,这就会导致计算出来的Hash值相同的很多。
而设计者将key的哈希值的高位也做了运算(与高16位做异或运算,使得在做&运算时,此时的低位实际上是高位与低位的结合),这就增加了随机性,减少了碰撞冲突的可能性!
详细理解下HashMap的put()过程吧:
put源码中还可以挖掘更多细节,比如putTreeVal()在树中插入节点、treeifyBin()转变树结构等,感兴趣的可以多多研究,关于红黑树结构及原理,文章下面会介绍。
-
resize()方法–扩容机制:
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说&