1,HashMap的底层实现:数组+链表。
数组的特点:遍历查询速度较快,但增加和删除的代价较大。
链表的特点:遍历查询速度较慢,但增加和删除的代价较小。
而HashMap的底层是数组和链表,很好地集成了这两者的优点。
2,HashMap数组及链表的产生:
数组:当创建一个HashMap对象时,Map<k,v> map=new HashMap<k,v>();就会调用相应的空构造方法(HashMap有四种构造方法),返回一个默认初始容量为16,默认加载因子为0.75的table数组。数组中的元素是链表。
链表:链表产生的原因是发生了碰撞(哈希冲突)。至于为什么会发生碰撞呢?在向HashMap存储数据的过程中,有两种情况:第一种是在某一链表的表头table[bucketIndex]插入键值对元素;另一种情况是新加入的键值对元素覆盖了此前的键值对元素。第一种情况就会造成碰撞,从而形成链表。
3,HashMap的数据结构:
HashMap的底层实现是一个Entry数组(Entry<K,V>[ ] table),而且数组中的每一个元素都是链表(由多个Entry<K,V>连接而成的链表)。
先来看看每一个Entry<K,V>的结构:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* 构造
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
可以看出:Entry元素中有四个量,Key,Value,hash和next,通过next来构成连接的关系从而形成链表。每个元素的next都是对下一个元素的引用。
4,HashMap的重要方法实现:
(1)存储实现put(Key k, Value v):
底层代码:
public V put(K key, V value) {
//key==null
if (key == null)
return putForNullKey(value);
//key!=null
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
HashMap的存储分为两种情况:
1)传入的key参数为null:
//key==null
if (key == null)
return putForNullKey(value);
再来看看putForNullKey方法:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
由此可以看到,当传入的key为null时,就会调用putForNullKey方法。底层就会找到table数组的第一个下标的头结点(Entry<K,V> e = table[0])。如果头结点存在即不为空时,就遍历这个链表,寻找有没有key为null的结点:如果找到了,就把传入的value值覆盖此前这个位置结点的value,并且返回旧的value值;如果在遍历的过程中没有找到key为null的结点,就执行addEntry()方法。
来看看addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
还有createEntry方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
调用addEntry方法后的第一步就是判断当前容量是否超出了极限容量,如果超出了就增大数组的长度为原来的两倍,并且重新计算哈希值,再调用createEntry方法。从createEntry方法中我们可以知道,Entry<K,V> e = table[bucketIndex]语句找到了table数组某个索引下的头结点,在这个位置创建了一个新的键值对元素。这说明,如果在遍历的过程中没有找到key为null的结点时,就会在这个链表的表头创建一个新的结点元素,这个结点的key为null。
所以总结起来,当传入的key为null时,新的键值对可能被存储在table[0]的链表头,也可能被存储在table[0]链
表中的某一结点位置。
2)传入的key参数不为null:
来看看源码:
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
当传入的key不为null时,底层会先把key所对应的hash值求出来,再根据这个值来求出它在table数组中的索引i。找到这个索引后,就通过Entry<K,V> e = table[i]来判断这个索引下链表的表头是否为空。如果表头不为空,则遍历这个链表,查找链表中是否存在某个元素的hash值与key值都与这个新插入的元素相匹配。如果找到,直接替换value并返回旧的value。如果找不到,就会执行头插(在链表头插入这个新元素)。
经过了简单的分析,很容易得出HashMap的存储主要是实现两种方式:替换和头插。
(2)读取实现get(key):
先来看看源码
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {
Object k;
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
读取的实现和存储有类似之处。先把key值转化为对应的hash值,通过这个值来计算出对应在table数组的哪个索引。如果索引处的表头不为空,就遍历这个链表来寻找hash值、key值均匹配的元素。找得到就返回这个元素e,否则返回null。最后通过entry.getValue()来获取这个key所对应的值。
(3)HashMap中键的遍历:
这里只简单地说一个特点:遍历出来的结果是无序的。
这和HashMap的存储有关。最后一个存储的元素应该是第一个或者最后一个被遍历的,但是这最后一个存储进去的元素可能在表头,也可能在链表中间的某一个位置,因此就会造成这种无序性。