HashMap基本结构
我们常见的两种数据结构:
数组:数据存储地址连续。查询快,寻址容易。但是插入删除困难
链表:数据散列存储。查询慢,但是增删快
上述两个结构各有优缺,HashMap 就是将这两种结构进行结合,即采用数组+链表的形式,其中,每一个数组中的值存放的是一个Entry类,属性有key(键),value(值),next(下一个)。
HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键
部分源码如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
从源码可以看出,HashMap 里实现一个静态内部类Entry,Entry就是数组中的元素,其重要的属性有 key , value, next,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,从而构成了链表。
HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
HashMap 实现存储与读取
由上面我们知道,HashMap 将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对。
因此,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,再根据equals() 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据 equals() 方法从该位置上的链表中取出该Entry。
1. 存储:
部分源码如下:
public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
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;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
由源码可以看出,当 HashMap 需要存储一个 key-value 对时,会根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置(即这个元素在数组中的下标),如果数组该位置上已经存放有其他元素了(即hashCode() 返回值相同),那么,若这两个 Entry 的 key 通过 equals() 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。若这两个 Entry 的 key 通过 equals 比较返回 false,在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
其中:
- hashCode() 函数:
将想存放在 HashMap 中的 key 转化为数组中的index,最后再对数组长度取余
int hash = key.hashCode();
int index = hash%num.length;
其中,取余的目的时,如果 hashCode() 得到的 index 较大的话,则需要较大的数组空间,所以进行取余,便于将较大的数据放在较小的数组空间中 - equal() 函数:
equal() 函数在 HashMap 中被重写了,而且如果要重写 equal() 必须重写 hashCode(),通过这两个函数来确定hashMap中存入键值对的唯一性。
hashmap中约定,如果两个对象 equal() 相等,那么这两个对象产生的 hashcode() 也应该相等。若 equal() 不等,hashcode() 可能相等。
hashcode() 是根据对象的地址空间取计算的,String等很多对象都是重写的 equal() 方法,变成了值相等,所以如果你不重写,那么 equal() 出来的对象可能是一样的,但是地址空间却又不一样。这与 HashMap 中的约定相矛盾。
2. 读取
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
从 HashMap 中get元素时,首先计算 key 的 hashCode,找到数组中对应位置的某一元素,然后通过 key 的 equals() 方法在对应位置的链表中找到需要的元素。
解决 hash 冲突
哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。若两个不同的元素,通过哈希函数得出的实际存储地址相同,也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,这就是所谓的哈希冲突,也叫哈希碰撞。
解决 hash 冲突的方法有:
- 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
- 再哈希法
- 链地址法
- 建立一个公共溢出区
其中,HashMap 的解决冲突办法为:链地址法
再散列resize过程
当 HashMap 中的元素越来越多的时候,碰撞的几率也就越来越高,所以为了提高查询的效率,当哈希表的容量超过默认容量时,必须调整 table 的大小。需要通过 resize 对 HashMap 的数组进行扩容。创建一张新表,容量扩大至原数组的两倍,原数组中的数据必须重新计算其在新数组中的位置,映射到新表中。
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
//重新计算index
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}