HashMap实现原理
HashMap是基于散列表的Map接口的实现。插入和查询的性能消耗是固定的。可以通过构造器设置容量和负载因子,一调整容易得性能。
散列表:给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。HashMap中散列表由数组实现。
容量:散列表数组的长度。
负载因子:散列表中当前存储的项/容量。HashMap默认使用的负载因子是0.75。
HashMap是键-值对结构。HashMap的键不能重复(可以是null),而值可以重复。在Java中如果一个类作为HashMap的key要能正确的工作,那么这个类就需要同时实现hashCode()方法和equals()方法。
HashMap使用equals()判断当前键是否与表中存在的键相同。使用hashCode()生成散列码。hashCode()就是散列函数(也称为哈希函数)。
正确的equals()方法必须满足下列5个条件:
- 自反性:对任意x,x.equals(x)一定返回true
- 对称性:对任意x,y,如果x.equals(y)返回true,则y.equals(x)也返回true
- 传递性:对任意x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也返回true
- 一致性:对任意x,y,如果对象中用于等价等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致。
- 对任何不是null的x,x.equlas(null)一定返回false
HashMap通过散列的方式决定如何存储键以达到更快的查找速度。
首先看一下HashMap是如何表示一个键-值对的对象的。
Map.java
public interface Map<K, V> {
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
/// ......
}
/// ......
}
在Map.java中定义了Entry<K, V>接口表示一个键-值对。具体的实现由Map的实现类定义。
HashMap.java
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
return o instanceof Map.Entry<?, ?> e
&& Objects.equals(key, e.getKey())
&& Objects.equals(value, e.getValue());
}
}
/// ......
}
HashMap基于散列表实现,在Java中使用一个数组表示散列表。通过散列将键信息(就是Map.Entry<K,V>对象)保存在数组中。散列通过键对象生成一个数字,将其作为数组的下标。这个数字就是散列码。
在调用HashMap的put方法时,首先通过散列计算散列码得到数组的下标,然后查询指定下标的数组位置上是否有值(Map.Entry<K,V>),如果没有值则将put的键-值对生成Map.Entry<K,V>对象存在该位置。如果有值则对比当前put的值是否已经存在,如果存在则替换,不存在则将put的键-值对生成Map.Entry<K,V>对象添加到最后一个Map.Entry<K,V>的next域上。
在调用HashMap的get方法时,同样先计算散列码得到数组的下标然后查询该位置的值,如果不存在则返回null,存则查找Map.Entry<K,V>链,直到找到对应键的值返回。