初探HashMap
1、数组解决了什么问题?
在讲HashMap之前先讲一下数组,因为这两种数据结构有着相同的目的。首先我们考虑一种场景:“小明有哈利波特1-100部的书(这里假设哈利波特有100部),书架上有100个位置,如何摆放能让小明可以快速的找到任意一部哈利波特呢?”。这时候小明想到一个简单高效的办法,给每个位置上贴上一个标签,1,2,3…,100,1对应第一部,2对应第二部,以此类推,这样想找任意一部的时候通过索引就可以找到了。这里模拟的就是数组的随机访问,只不过数组的下标是从0开始。数组之所以从0开始,可以这么理解,数组每个元素都占有相同的内存空间,可以把数组的定位模拟成一个等差数列,如果下标从1开始等差数列第N项公式 an = a1 + (n-1)d, 这里d可以抽象成每个元素的内存空间,比如对于int就是4个字节,而每个数组的引用其实就是代表每个数组第一个元素的地址,比如 int[] a = new int[]{1,2,3};,其中a就代表的这个数组的首地址,如果下标从0开始,an = a0 + nd,减少了一道运算,而这种基本数据类型的运算应该要尽量优化到极致,所以下标从0开始。这样通过计算公式,我们就可以很方便的通过映射找到数组每个元素的位置,从而可以取出每个元素。我们可以说,数组可以通过下标实现随机访问。
2、为什么需要HashMap?
讲了这么多数组,但是这与HashMap有什么关系呢?在讲HashMap之前来看一下Map,Map意思有地图,测绘,还有映射的意思,这里的Map指的就是映射。
提到了映射,这里我们发现与数组有一点关系了,因为数组不就是通过解决索引跟值的映射,从而实现随机访问的吗?
那为什么还需要有Map呢?因为现实中JAVA的键值对实在是太复杂了,并不是每个场景都跟“小明与他的哈利波特”一样,如果是《哈利波特》1-100跟《网球王子》1-10呢?这个时候我们怎么用下标表示呢?如果对象不是书,而是人呢?
在java中,每个对象都有自己的地址,通过地址的来作数组下标看上去是一个不错的选择,但是仔细想想有如下问题:
- jvm中对象的内存地址其实会不断发生变化的,举个例子:JVM在垃圾回收时,会把eden区和from区中存活下来的对象复制到to区域,这个时候对象的地址一定会发生变化,如果采用地址做下标,这个时候数组的定位就不准确了;
那么为什么不用HashCode作为下标呢?事实上,用HashCode作为下标也会有问题:
- 1、HashCode计算出来的数据非常大,如下图所示,随便一个对象的HashCode都是很巨大的数字,如果通过这个做下标,会造成空间的巨大浪费。
- 2、HashCode有一定的冲突,也就是说不同对象的HashCode 是有可能相同的,这样通过HashCode作为下标,可能会有一定冲突。
总结:综合1,2我们可以得出,数组这么简单的数据结构并不能满足复杂的映射关系,那有没有什么改进之处呢?
- 对于1我们可以把hashcode对数组长度取模,这样可以将大数映射到小数上,但是依旧没有解决冲突问题。
- 对于2我们可以用开放寻址,或者拉链法来解决冲突,这时HashMap应运而生。
3、HashMap
通过上述总结我们可以知道,通过散列函数(上述举例其实是一个最简单的散列函数,取模)可以缩小映射范围到我们数组的范围内。处理冲突的方式有很多:开放寻址、拉链法、再散列法等等。
HashMap的做法就是如此,内部维护了一个数组,使用自己的散列函数定位到数组的位置,然后在对应的位置放入元素。如果冲突,先构造成链表,当链表长度大于8时,重新构造为红黑树。可以说,HashMap采用的是比较高级的“拉链法”。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//如果数组为空,则直接初始化table数组.
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//如果位置i没有被占用,直接放table[i]中.
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//Key值冲突,注意这里不是hash冲突,比如原来map中含有key为"123",后来又执行map.put("123")
//p代表原来key为"123"对应的value;
e = p;
else if (p instanceof TreeNode)
//如果p是红黑树节点,通过红黑树新增这个节点.
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//不是上述两种情况,就一定是链表,找到链表尾节点,插入新节点.
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//链表中找到重复元素,直接跳出循环
break;
p = e;
}
}
if (e != null) { // existing mapping for key
//存在如果重复的key.
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
PS:最开始看到bitMap数据结构的时候在想,为什么明明是一个位数组,却取名叫bitMap?为什么不叫bitArray呢?后来从map和array的实现与目的出发思考,可以理解为map其实就是一种更加通用的array,array规定下标是数字,map的“下标”可以是任何东西