笔者最近发现java里面的Map集合比较有意思,而且经常被拿来面试用,所以这里写几篇关于Map集合的博客与大家讨论
首先是一张Map集合的框架图(这张图网上引用的)
接下来的几篇博客,我会从比较重要的HashMap、Hashtable、TreeMap、ConcurrentHashMap等集合常见的Map集合来介绍
·HashMap
·基本概念
HashMap是一个很常见的Map,它是根据键的HashCode值进行存储数据,根据键可以直接获取它的值。这里一些Map常见的方法就不介绍了,HashMap实现了Map接口,继承AbstractMap。Map定义键值映射的规则,而AbstractMap提供了Map接口的骨干实现。
·两个重要的参数
在HasMap中有两个重要的参数,初始容量和加载因子,这两个参数是影响HashMap性能的重要参数,其中容量表示的是哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前的一种衡量的尺度,加载因子表示的是散列表的装填程度大小。这两个概念是HashMap中的比较重要的概念,后面会重点介绍。
·HashMap的结构(jdk1.7的)
其中每个链表都可以看成一个桶。插入元素的时候,首先将键传入一个哈希函数,函数通过散列的方式告知元素属于哪个桶,然后在相应的链表头插入元素。查找和删除元素的时候,用同样的方式先找到元素的桶,然后遍历相应的链表,直到发现我们想要的元素。
下面通过源码来介绍一下,put和get方法
public V put(K key, V value) {
//当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
if (key == null)
return putForNullKey(value);//计算key的hash值
int hash = hash(key.hashCode()); ------(1)
//计算key hash 值在 table 数组中的位置
int i = indexFor(hash, table.length); ------(2)
//从i出开始迭代 e,找到 key 保存的位置
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
//判断该条链上是否有hash值相同的(key相同)
//若存在相同,则直接覆盖value,返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; //旧值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; //返回旧值
}
}
//修改次数增加1
modCount++;
//将key、value添加至i位置处
addEntry(hash, key, value, i);
return null;
}
可以看出先判断key是否为空,若为空则直接调用putForNullKey方法
若不为空,则先计算key的hash值,然后通过hash值搜索table数组中的索引位置,如果table数组在该位置有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。这里又引入了两个新的方法indexFor方法和hash方法。
indexFor方法
里面有一条语句 h&(length-1)
hash方法
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
首先来解释一下indexFor方法,这个方法很有意思,它的作用其实是为了均匀分布table数据和充分利用空间
这里引入一张图片
可以看出来当length长度为15时,有很多的结果值是一样的,也就是他们会产生碰撞,相反我们想一下,当length长度为16的时候,就不会减少这个碰撞的几率,15就是1111,做&运算的时候,值总是与原来hash值相同,这样就会使得table数组中的数据分布的均匀,查询速度也较快。
·加载因子问题
随着HashMap中的元素越来越多,产生碰撞几率就会越来越大,势必会影响HashMap的速度,所以当桶的数量=table数组的长度*加载因子的时候,就会扩容,这里扩容长度是*2。扩容会引来一些问题,这样会重新计算这些数据在新的table数组中的位置,并复制处理,所以使用hash容器时尽量预估自己的数据量来设置初始值。这里其实细节有很多,可以去csdn上看看其他文章,有介绍的很详细的。
这里又有人会问了,为什么加载因子是0.75。。。emmm笔者认为应该从空间和时间上综合考虑,才采用的默认因子为0.75吧
·jdk1.8中的HashMap
上面我们说jdk1.7中的HashMap采用一个Entry数组来存储数据,用key的hashcode取模来决定回放到哪个数组里,然后如果hashcode相同,或者hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。所以可以说查询数据的时间复杂度为O(1+n)
而1.8中的HashMap使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构,如果同一个格子里的key不超过8个,使用链表结构存储;如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。所以查询的时间复杂度为O(1+logn)比1.7的效率有了一定的提升