哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张答的哈希表,而HahsMap的实现原理也常常出现在各类的面试题中,重要性可见一斑。
HashMap介绍
HashMap简介
HashMap
是一个散列表,它存储的内容是键值对(key-value)映射。HashMap
继承于AbstractMap
,实现了Map
、Cloneable
、java.io.Serializable
接口。HashMap
的实现不是同步的,这意味这它不是线程安全的。它的key
、value
都可以为null。此外,HashMap
中的映射不是有序的。HashMap
的实例有两个参数影响其性能:初始容量
和加载因子
。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash
操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是0.75
,这是在时间和空间成本上寻求一种折衷,加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数HashMap类的操作中,包括get
和put
操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash
操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生rehash
操作。
HashMap的构造函数
HashMap
共有4个构造函数,如下:
HashMap的API
|
|
HashMap数据结构
HashMap的继承关系
|
|
HashMap与Map关系如下图
:
从图中可以看出:
1) HashMap
继承于AbstractMap
类,实现了Map接口。Map是”key-value键值对”接口,AbstractMap
实现了”键值对”的通用函数接口。
2) HashMap
是通过”拉链法”实现的哈希表。它包括几个重要的成员变量:table
, size
, threshold
, loadFactor
, modCount
。table
是一个Entry[]
数组类型,而Entry
实际上就是一个单向链表。哈希表的”key-value键值对”都是存储在”Entry”数组中的。size
是HashMap
的大小,它是HashMap
保存的键值对的数量。threshold
是HashMap
的阈值,用于判断是否需要调整HashMap
的容量。threshold
的值=”容量*加载因子”, 当HashMap
中存储数据的数量达到threshold
时,就需要将HashMap
的容量加倍。loadFactor
就是加载因子。modCount
是用来实现fail-fast
机制的。
HashMap源码解析(基于JDK1.6.0_45)
为了更了解HashMap的原理,下面对HashMap源码代码作出分析。
在阅读源码时,建议参考后面的说明来建立对HashMap的整体认识,这样更容易理解HashMap。
|
|
说明
:HashMap
就是一个散列表,它是通过拉链法
解决哈希冲突的。影响
HashMap
性能的有两个参数:初始容量(initialCapacity)
和加载因子(loadFactor)
。容量是哈希表中桶的数量,初始容量
只是哈希表在创建时的容量。加载因子
是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash
操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
HashMap的”拉链法”相关内容
HashMap数据存储数组
|
|
HashMap中的key-value
都是存储在Entry数组
中的。
数据节点Entry的数据结构
|
|
从中,我们可以看出Entry实际上就是一个单向链表。这也是为什么我们说HashMap是通过拉链法解决哈希冲突的。
Entry实现Map.Entry接口,即实现getKey()
、getValue()
、setValue(V value)
、equals(Object o)
、hashCode()
这些函数。这些都是基于的读取/修改key
、value
值的函数。
HashMap的构造函数
HashMap共包括4个构造函数
|
|
HashMap的主要对外接口
clear()
clear()
的作用是清空HashMap
。它是通过将所有的元素设为null来实现的。
containsKey()
containsKey()
的作用是判断HashMap是否包含Key。
containsKey()
首先通过getEntry(key)
获取key
对应的Entry
,然后判断该Entry
是否为null。getEntry()
的源码如下:
getEntry()
的作用就是返回”键为key”的键值对,它的实现源码中已经进行了说明。
这里需要强调的是:HashMap将”key为null”的元素都放在table的位置0
处,即table[0]
中:”key不为null”的放在table的其余位置!
containsValue()
containsValue()
的作用是判断HashMap是否包含值为value
的元素。
从中,我们可以看出containsNullValue()
分为两步进行处理:第一,若”value为null”,则调用containsNullValue()
。第二,若”value不为null”,则查找HashMap
中是否有值为value的节点。containsNullValue()
的作用判断HashMap
中是否包含”值为null”的元素。
entrySet()、values()、keySet()
它们三个的原理类似,这里以entrySet()
为例来说明。entrySet()
的作用是返回”HashMap中所有Entry的集合”,它是一个集合。实现代码如下:
|
|
HashMap
是通过拉链法实现的散列表。表现在HashMap包括许多的Entry,而每一个Entry本质上又是一个单向链表。那么HashMap遍历key-value键值对的时候,是如何逐个去遍历的呢?entrySet()
实际上是通过newEntryIterator()
实现的。
|
|
当我们通过
entrySet()
获取到的Iterator
的next()
方法去遍历HashMap
时,实际上调用的是nextEntry()
。而nextEntry()
的实现方式,先遍历Entry
(根据Entry在table中的序号,从小到大的遍历);然后对每个Entry(即每个单向链表),逐个遍历。
get()
get()
的作用是获取key
对应的value
,它的实现代码如下:
put()
put()
的作用是对外提供接口,让HashMap
对象可以通过put()
将”key-value”添加到HashMap
中。
|
|
若要添加到
HashMap
中的键值对对应的key
不在HashMap
中,则将其添加到该哈希值对应的链表中,并调用addEntry()
|
|
addEntry()
的作用是新增Entry。将”key-value”插入指定位置,bucketIndex
是位置索引。
说到addEntry()
,就不得不说另一个函数createEntry()
。createEntry()
的代码如下:
它们的作用都是将key
、value
添加到HashMap
中。而且,比较addEntry()
和createEntry()
的代码,我们发现addEntry()
多了两句:
那么它们的区别是什么呢?
阅读代码,我们可以发现,它们的使用情景不同。
1) addEntry()
一般用在新增Entry可能导致”HashMap的实际容量”超过”阈值”的情况下。
例如,我们新建一个HashMap
,然后不断通过put()
向HashMap
中添加元素;put()
是通过addEntry()
新增Entry的。
在这种情况下,我们不知道何时”HashMap的实际容量”会超过”阈值”;
因此,需要调用addEntry()
。
2) createEntry()
一般用在新增Entry不会导致”HashMap的实际容量”超过”阈值”的情况下。
例如,我们调用HashMap”带有Map”的构造函数。它会将Map的全部元素添加到HashMap
中;
但在添加之前,我们已经计算好”HashMap的容量和阈值”。也就是,可以确定”即使将Map中的全部元素添加到HashMap中,都不会超过HashMap的阈值”。
此时,调用createEntry()
即可。
putAll()
putAll()
的作用是将m
的全部元素都添加到HashMap
中,它的代码如下:
|
|
remove()
remove()
的作用是删除”键为key”元素
|
|
HashMap实现的Cloneable接口
HashMap
实现了Cloneable
接口,即实现了clone()
方法。clone()
方法的作用很简单,就是克隆一个HashMap
对象并返回。
|
|
HashMap实现的Serializable接口。
HashMap
实现java.io.Serializable
,分别实现了串行读取、写入功能。
串行写入函数是writeObject()
,它的作用是将HashMap
的”总的容量,实际容量,所有的Entry”都写入到输出流中。
而串行读取函数是readObject()
,它的作用是将HashMap
的”总的容量,实际容量,所有的Entry”依次读出。
|
|
HashMap遍历方式
遍历HashMap的键值对
第一步:根据entrySet()
获取HashMap的”键值对”的Set集合。
第二部:通过Iterator
迭代器遍历”第一步”得到的集合。
遍历HashMap的键
第一步:根据keySet()
获取HashMap
的”键”的Set集合。
第二步:通过iterator
迭代器遍历”第一步”得到的集合。
遍历HashMap的值
第一步:依据value()
获取HashMap
的”值”的集合。
第二步:通过Iterator
迭代器遍历”第一步”得到的集合。
遍历测试程序如下:
|
|
HashMap示例
下面通过一个实例学习如何使用HashMap
。
|
|
运行结果如下: