HashMap在JDK1.7版本中的数据存储结构实际上是一个 Entry, ?>[] EMPTY_TABLE数组
static final Entry, ?>[] EMPTY_TABLE = {};
// table就是HashMap实际存储数组的地方
transient Entry[] table = (Entry[]) EMPTY_TABLE;
显而易见,其中的每个元素又是一个链表
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
int hash;
...//省略后续代码
因此,Java7 HashMap的结构大致如下图
总结:简单来说,HashMap中的数据存储结构是个数组,而每个元素都是一个单向链表,链表中每个元素是一个Entry的内部类对象,每个对象包含四个属性:key,value,hash值,和用于单向链表的next
重要的成员变量:看一下其中的其他成员变量
// 默认的HashMap的空间大小16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认值是16
// hashMap最大的空间大小
static final int MAXIMUM_CAPACITY = 1 << 30;
// HashMap默认负载因子,负载因子越小,hash冲突机率越低
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//初始化的空数组
static final Entry, ?>[] EMPTY_TABLE = {};
// table就是HashMap实际存储数组的地方
transient Entry[] table = (Entry[]) EMPTY_TABLE;
// HashMap 实际存储的元素个数
transient int size;
// 临界值(超过这个值则开始扩容),公式为(threshold = capacity * loadFactor)
int threshold;
// HashMap 负载因子
final float loadFactor;
构造方法:从源码中可以看出HashMap一共有个四个构造,他们分别为
//1.默认构造,会调用默认默认空间大小16和默认负载因子0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//2.指定大小但不指定负载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//3.指定大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
//此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(2的30次方)
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
init();
}
//4.使用默认构造创建对象并将指定的map集合放入新创建的对象中
public HashMap(Map extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//初始化数组
inflateTable(threshold);
//添加指定集合中的元素
putAllForCreate(m);
}
上面四个构造实际上都是在使用第三个构造方法:类中有几个比较重要的字段:
//实际存储的key-value键值对的个数
transient int size;
//当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
int capacity
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
//threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,
//如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
从源码中不难看出,实际上,在构造器中(第四个除外),并没有为数组分配内存空间,而是在put操作的时候才进行数据的构建
put操作
下面看下put方法的执行过程
- 首先要判断数字是否为空数组,如果是空数组的话,需要对数组进行初始化
- 如果key是null的话,回家元素的值仿佛唉table的0索引上,此时终止操作
- 如果key值不是null的话
- 对key进行hash操作,获取到hash值
- 找到key对应的数组下标
- 获取到链表对象后遍历链表,看是否有重复的key存在,如果有直接覆盖并返回原来位置上的值,就此结束
- 如果不存在重复的key,将此该key和value组装程Entry对象添加到链表中(存在数组扩容问题-后面有介绍)
//put操作源码
public V put(K key, V value) {
// 当插入第一个元素的时候,需要先初始化数组大小
if (table == EMPTY_TABLE) {
// 数组初始化
inflateTable(threshold);
}
// 如果 key 为 null,最终会将这个 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 求 key 的 hash 值
int hash = hash(key);
// 2. 找到对应的数组下标
int i = indexFor(hash, table.length);
// 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // key -> value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 4. 不存在重复的 key,将此 entry 添加到链表中
addEntry(hash, key, value, i);
return null;
}
数组初始化
ps:在添加元素的开始,需要对数组是否初始化做判断,如果没有初始化需要做初始化处理
保证数组大小是是2的N次方的好处:
当数组长度为2的n次幂的时候, 1、位移运算效率较高 2、不同的key的hash计算结果相同的几率较低,减少hash碰撞,使得数据在数组上分布的比较均匀, 查询的时候就不用遍历某个位置上的链表,可以提升定位元素的的效率
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 保证数组大小一定是 2 的 n 次方。
// new HashMap(519),大小是1024
//将数组大小保持为2的n次方,在Java7和Java8的HashMap和 ConcurrentHashMap 都有相应的要求,实现代码略有不同
int capacity = roundUpToPowerOf2(toSize);
// 计算扩容阈值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
// 确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
// 返回最接近临界值的2的N次方
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
计算元素在数组的具体位置
//简单说就是取 hash 值的低 n 位。如在数组长度为 32 的时候,其实取的就是 key 的 hash 值的低 5 位,作为它在数组中的下标位置
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
// 简单理解就是hash值和长度取模
return h & (length - 1);
}
添加到链表
ps:找到数组位置之后,就需要对key进行判重处理,如果有的话,就覆盖重复key的值,返回旧值(判断重复逻辑为:可以的hash值相同,且原来key和当前key相等),如果没有重复值,将新值放在链表的表头
//主要逻辑为,先判断是否需要扩容,如果需要扩容就先扩容,最后再把数据封装程Entry对象加入到链表表头
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容,容量 * 2
resize(2 * table.length);
// 扩容以后,重新计算 hash 值
hash = (null != key) ? hash(key) : 0;
// 重新计算扩容后的新的下标
bucketIndex = indexFor(hash, table.length);
}
// 创建元素
createEntry(hash, key, value, bucketIndex);
}
// 将新值放到链表的表头,然后 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
数组扩容
长度为当前长度的2倍
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果之前的HashMap已经扩充到最大了,那么就将临界值threshold设置为最大的int值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新的数组
Entry[] newTable = new Entry[newCapacity];
// 将原来数组中的值迁移到新的更大的数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
// 阈值计算
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//数组的拷贝
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历旧的数组
for (Entry e : table) {
while (null != e) {
//获取下一个entry对象
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//当前对象的hash值
int i = indexFor(e.hash, newCapacity);
//头插法,Entry对象放在新数组上第一位置,其他对象放在该对象的后一位置
e.next = newTable[i];
//将整体的对象放在指定的索引位置
newTable[i] = e;
//继续循环下一个Entry
e = next;
}
}
}
get方法跟踪
- 根据key计算出key的hash值
- 找到对应的数组下标
- 遍历该数组下的链表,直到找到与之相等的key的值,或找不到返回null
//获取数据
public V get(Object key) {
if (key == null)
//如果key为null,就从table[0]获取(put中,key为null也是存储在该位置)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
// 从链表中查询数据
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 确定key对应的数组位置,遍历链表直至找到,或者最终找不到返回null
for (Entry 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;
}