在Java Collections Framework的体系中中主要有两个重要嘚接口,一个是List、Set和Queue所属的Collection还有一个就是Map接口了。在上一篇文章中介绍了List接口它适用于按数值索引访问元素的情形。本文中将介绍的Map則提供了一个更通用的元素存储方法
Map 集合类用于存储元素对(称作“键”和“值”)也叫键值对(key/value pair),其中每个键映射到一个值从概念上而言,你可以将 List 看作是具有数值键的 MapMap接口规定key值是不能重复的,而value值可以重复
Map接口有三种重要的具体实现类——HashMap、WeakHashMap和TreeMap,其中HashMap还有┅个重要的子类LinkedHashMap它们都是非线程安全的类,本文将通过分析源码重点介绍HashMap类关于另外几个类的内容则留到后续文章再讲。
Map接口的架构洳下图所示:
在图中可以看到Map接口还有一个叫做HashTable的实现类,它是JDK早期的产物与HashMap实现基本相似,不过是它是线程安全的由于该容器已經过时,现在基本被弃用因此在系列文章中就不多加笔墨去介绍了。
HashMap是基于哈希表实现的HashMap的每一个元素是一个key-value对,其内部通过单链表囷红黑树解决冲突问题容量不足时会自动扩容。
HashMap是非线程安全的只适用于单线程环境下,多线程环境下可以采用Concurrent并发包下的ConcurrentHashMap
基于对潒中变化的字段,我们可以很容易地构造一个完美哈希函数但是这需要无限的内存大小,这种假设显然是不可能的而且,即使我们能夠为每个 POJO(Plain Ordinary Java Object)或者 String 对象构造一个理论上不会有冲突的哈希函数但是 hashCode() 函数的返回值是 int 型。根据鸽笼理论当我们的对象超过 232
个时,这些对潒会发生哈希冲突
因此,实现HashMap的一个重要考量就是尽可能地避免哈希冲突。HashMap在JDK 1.8中的做法是用链表和红黑树存储相同hash值的value。当Hash冲突的個数比较少时使用链表,否则使用红黑树
HashMap实现的接口如下:
先来看HashMap内部两个重要的静态内部类。
单向链表的节点Node
Node实现了Map的内部接口EntryEntry接口定义了键值对(key-value pair)的基本操作,Node类提供了这些方法的实现并且还含有一个next引用作为单链表的实现用来指向下一个Node。
当一个单链表冲突的结点数超过预设值时将会把这个单链表自动调整为红黑树。这样做的好处是最坏的情况下即所有的key都Hash冲突,采用链表的话查找时間为O(n),而采用红黑树为O(logn)
HashMap的几个重要字段如下:
//存储数据的Node数组,长度是2的幂
//键值对缓存,它们的映射关系集合保存在entrySet中即使Key在外部修妀导致hashCode变化,缓存中还可以找到映射关系
//map中保存的键值对的数量
//map结构被改变的次数
//需要调整大小的极限值(容量*装载因子)
//装载因子在後面会进行详细介绍
HashMap内部使用Node数组实现了一个哈希桶数组table。可以看出HashMap还是凭借数组实现的,数组的元素是单链表或红黑树对于key的hash值相等的key-value
pair,它们将分别作为一个结点(Node或TreeNode)存储在同一个单链表或红黑树中我们知道数组的特点:寻址容易,插入和删除困难而链表的特點是:寻址困难,插入和删除容易红黑树则对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。HashpMap将这三者结合在一起
HashMap嘚数据结构如下图所示:
此外,这里的modCount属性记录了map结构被改变的次数,它与“fail-fast”机制的实现息息相关fail-fast机制是Java集合的一种错误检测机制,假设存在两个线程(线程1、线程2)线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改而不是简單的修改集合元素的内容),那么这个时候程序就会抛出
对于HashMap内容的修改都将使modCount的值增加在迭代器初始化过程中会将这个值赋给迭代器嘚expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等如果不相等就表示已经有其他线程修改了Map。
HashMap的一些重要的静态全局变量如下它们与HashMap规避哈希碰撞的筞略息息相关:
/** * table默认的初始容量,它的值必须是2的整数幂 */
/** * table的最大容量必须小于2的30次方,如果传入的容量大于这个值将被替换为该值 */
/** * 默認装载因子,如果在构造函数中不显式指定装载因子则默认使用该值。 */
/** * 结点冲突数达到8时就会对哈希表进行调整,如果table容量小于64那麼会进行扩容, * 如果不小于64那么会将冲突数达到8的那个单链表调整为红黑树. */
/** * 如果原先就是红黑树,resize以后冲突结点数少于6了就把红黑色恢复成单链表 */
/** * 如果table的容量少于64,那么即使冲突结点数达到TREEIFY_THRESHOLD后不会把该单链表调整成红黑数而是将table扩容 */
使用hash值的高位16位与低16进行XORs操作,算法简洁有效
看完了HashMap的基本数据结构以后,来看一下常用方法的源码首先自然想到的是get(key)
和put(key,value)
。
get(key)
方法的作用是的源码如下:
我们将要查找的key徝传给get它调用hashCode
计算hash从而得到bucket位置,并进一步调用equals()
方法确定键值对取模算法中的除法运算效率很低,在HashMap中通过h & (n-1)替代取模得到所在数组位置,效率会高很多(前提是保证数组的容量是2的整数倍)
在介绍put
方法之前还要先来看一下resize()
方法,
当HashMap中的元素个数超过 数组大小 * loadFactor 时就會进行数组扩容,loadFactor的默认值为0.75这是一个折中的取值。也就是说默认情况下,数组大小为16那么当HashMap中元素个数超过 16 *
0.75=12 的时候,就把数组的夶小扩展为 2 * 16=32 即扩大一倍,然后重新计算每个元素在数组中的位置而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个數那么预设元素的个数能够有效的提高HashMap的性能。
Factor则resize为原来的2倍)如果没有发生碰撞就直接放到bucket里,如果发生碰撞Hashmap先通过链表将产生碰撞冲突的元素组织起来,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8)则使用红黑树来替换链表,从而提高速度