HashMap是平时最常用到的数据结构之一,查找和增删元素具有O(1)的时间复杂度。
本文将用一个简单的例子来解释下scala语言中HashMap内部的工作原理,看下get和put到底是怎样工作的。
用法示例
def main(args: Array[String]): Unit = {
val m = mutable.HashMap[String,Int]()
m.put("a",1)
m.put("b",2)
println("m:"+m)
println("a="+m.get("a"))
for( (k,v) <- m){ //遍历方式
print(k,v)
}
}
输出结果:
m:Map(b -> 2, a -> 1)
a=Some(1)
(b,2)(a,1)
HashMap实现原理
1、数据结构
HashMap是数组和链表相结合,其中key的hash值是数组的下标,同样hash值的key以链表方式存储。
其中数组用table, 其中table数组存储了Entry类的对象,
Entry的key即HashMap的键,value即HashMap的值,next为下一个Entry元素
@transient protected var table: Array[HashEntry[A, Entry]] = new Array(initialCapacity)
trait HashEntry [A, E] {
val key: A
var next: E = _
}
final class DefaultEntry[A, B](val key: A, var value: B)
extends HashEntry[A, DefaultEntry[A, B]] with Serializable
{
override def toString = chainString
def chainString = {
"(kv: " + key + ", " + value + ")" + (if (next != null) " -> " + next.toString else "")
}
}
2、put方法
我们看一下put的代码实现:
override def put(key: A, value: B): Option[B] = {
val e = findOrAddEntry(key, value)
if (e eq null) None
else { val v = e.value; e.value = value; Some(v) } //如果key存在则将其value值进行更新
}
/** Find entry with given key in table, or add new one if not found.
* May be somewhat faster then `findEntry`/`addEntry` pair as it
* computes entry's hash index only once.
* Returns entry found in table or null.
* New entries are created by calling `createNewEntry` method.
*/
protected def findOrAddEntry[B](key: A, value: B): Entry = {
val h = index(elemHashCode(key)) //获取hash值计算出的数组的下标
val e = findEntry0(key, h) //通过下标和键查找value是否存在
if (e ne null) e else { addEntry0(createNewEntry(key, value), h); null }
}
private[this] def findEntry0(key: A, h: Int): Entry = {
var e = table(h).asInstanceOf[Entry]
while (e != null && !elemEquals(e.key, key)) e = e.next
e
}
首先需要根据hashMap的键key计算出table数组的下标h,
然后找到table中的第h元素e,当e不为空并且e的key和HashMap的key相同时,将e的next赋给e
最后返回的e如果不为空,表示key存在,返回Entry对象,并将其value值进行更新
如果e为空,则进行添加
protected def createNewEntry[B](key: A, value: B): Entry
private[this] def addEntry0(e: Entry, h: Int) {
e.next = table(h).asInstanceOf[Entry] //将e作为链表的头结点
table(h) = e
tableSize = tableSize + 1
nnSizeMapAdd(h)
if (tableSize > threshold)
resize(2 * table.length)
}
每次添加元素的时候tableSize加一,代表元素的总个数,即size方法返回的结果
override def size: Int = tableSize
protected def elemHashCode(key: KeyType) = key.##
其中##是一个方法
##():Int
返回一个对象的hasCode,和hascode方法区别是:
对于numerics,它返回与值相等一致的散列值,
对于null,返回一个hashcode,而hashCode抛出NullPointerException。
2、table扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。
所以为了提高查询的效率,就要对HashMap的数组进行扩容,
这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:
原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小threshold时,
就会进行数组扩容,threshold的默认值为table大小的0.75,这是一个折中的取值。
也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,
就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,
private def resize(newSize: Int) {
val oldTable = table
table = new Array(newSize)
nnSizeMapReset(table.length)
var i = oldTable.length - 1
while (i >= 0) {
var e = oldTable(i)
while (e != null) {
val h = index(elemHashCode(e.key))
val e1 = e.next
e.next = table(h).asInstanceOf[Entry]
table(h) = e
e = e1
nnSizeMapAdd(h)
}
i = i - 1
}
threshold = newThreshold(_loadFactor, newSize)
}
private[collection] final def defaultLoadFactor: Int = 750 // corresponds to 75%
private[collection] final def loadFactorDenum = 1000 // should be loadFactorDenom, but changing that isn't binary compatible
private[collection] final def newThreshold(_loadFactor: Int, size: Int) =
((size.toLong * _loadFactor) / loadFactorDenum).toInt //新的threshold是扩容前大小两倍的75%
3、get方法
理解了put操作则get方法就会简单得多,get只是put操作其中的一步。
def get(key: A): Option[B] = {
val e = findEntry(key)
if (e eq null) None
else Some(e.value)
}
protected def findEntry(key: A): Entry =
findEntry0(key, index(elemHashCode(key)))
总结
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射.
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap的实现是借助于哈希表HashTable实现的