HashMap,日常最常用的数据结构之一。它是基于哈希表的 Map 接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。下面将通过源码分析存储结构、初始化、插入、查询、移除等来深入分析Hashmap的实现原理。
0.equal hashcode ==的区别
为了分析HashMap,我们首先应该理解hashCode及equal的区别,如下:
== |
内存地址比较 |
equal |
Object默认内存地址比较,一般需要复写 |
hashcode |
主要用于集合的散列表,Object默认为内存地址,一般不用设置,除非作用于散列集合。 |
(1)hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。当equals方法被重写时,通常有必要重写 hashCode 方法。
(2)但hashCode相等,不一定equals()
1.存储结构
HashMapde的存储结构是采用顺序存储结构及链式存储结构。顺序存储结构存储着每个链表的头结点。每个Key根据计算bucketindex来确定数组下标。
bucketindex=hash&(length-1)。当
bucketindex相同时,插入链表头部。
// Entry是单向链表。
// 它是 “HashMap链式存储法”对应的链表。
// 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
static class Entry implements Map.Entry {
final K key;
V value;
// 指向下一个节点
Entry next;
final int hash;
// 构造函数。
// 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
Entry(int h, K k, V v, Entry n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 否则,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 实现hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 当向HashMap中添加元素时,绘调用recordAccess()。
// 这里不做任何处理
void recordAccess(HashMap m) {
}
// 当从HashMap中删除元素时,绘调用recordRemoval()。
// 这里不做任何处理
void recordRemoval(HashMap m) {
}
}
2.初始化(加载因子)
HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,会调用方法将容量翻倍。所以这是时间和空间的矛盾,最后根据自己的业务来设定。
// 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// HashMap的最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因此不能小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 找出“大于initialCapacity”的最小的2的幂
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// 设置“加载因子”
this.loadFactor = loadFactor;
// 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
threshold = (int)(capacity * loadFactor);
// 创建Entry数组,用来保存数据
table = new Entry[capacity];
init();
}
3.bucketindex
HashMap中的数据结构是数组+单链表的组合,我们希望的是元素存放的更均匀,最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。所以可以采用%的方式,既哈希值%容量=bucketIndex。而源码的实现采用 h & (length-1),具有更高的效率。这里注意,为什么HashMap的默认容量要求2N次方。
当容量一定是2^n时,h & (length - 1) == h % length
4.put
HashMap添加元素主要先根据key的hash计算出bucketindex,如果该buckeindex下标的链表存在,则遍历进行替换,否则往数组添加新的链表。
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 若“该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;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
// 返回h在数组中的索引值,这里用&代替取模,旨在提升效率
// h & (length-1)保证返回值的小于length
static int indexFor(int h, int length) {
return h & (length-1);
}
// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry(hash, key, value, e);
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
5.get
HashMap根据key获取元素主要就是通过bucketindex找到链表,进行查询。
// 获取key对应的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key的hash值
int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值等于key”的元素
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//判断key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
//没找到则返回null
return null;
}
6.remove
// 删除“键为key”的元素
final Entry removeEntryForKey(Object key) {
// 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry prev = table[i];
Entry e = prev;
// 删除链表中“键为key”的元素
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
7.HashTable
(1)HashTable的实现原理基本与HashMap一致,除了细微的方法实现不一致外,所以不进行原理分析。
(2)HashTable为线程安全,HashMap为非线程安全
(3)HashMap可以接受为null的key和value,而Hashtable则不行
(4)HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常
(5)Java 5提供了ConcurrentHashMap(局部锁机制),它是HashTable的替代,比HashTable的扩展性、性能更好
8.总结
(1)HashMap是顺序结构及链表结构的组合
(2)最后根据业务设置容量及加载因子可以提高插入及查找效率,同时可以避免增容带来的效率问题
(3)HashMap为非线程安全
(4)HashMap的精髓为bucketindex(使元链表均匀分布在数组),可以提供空间的利用率及元素的插入及查找效率。
(5)由于hash相等,equal不一定相等。有可能导致撞库的问题,由于HashMap具有链表的功能。可以避免撞库问题。