一、JDK1.7HashMap介绍
JDK1.7中HashMap底层是数组+链表
几个默认值
- DEFAULT_INITIAL_CAPACITY 默认初始化容量,16
- MAXIMUM_CAPACITY 最大容量 2的30次幂
- DEFAULT_LOAD_FACTOR 默认的负载因子 0.75
- EMPTY_TABLE 空对象{}
几个属性值
- Entry<K,V>[] table:底层保存数据的数组
- int size:map的大小
- modCount:修改次数,用于快速失败
- threshold:扩容阈值
- loadFactor:负载因子
- hashSeed:哈希种子,用于计算hash值,使其更加散列
与JDK1.8HashMap的区别
- JDK7是数组+链表,JDK8是数组+链表+红黑树
- 在put数据时,JDK7采用头插法,JDK8采用尾插法
- 在put数据时,若需要扩容,JDK7先扩容再插入数据,JDK8先插入数据再扩容
- JDK7的hash()计算方式较JDK8复杂
- JDK7在扩容转移数组时倒序,多线程的情况下有可能会产生循环链表导致死锁的产生,JDK8在扩容转移数据时正序,不会出现这种情况
二:构造器
在调用HashMap的无参构造方法时,对初始容量赋值16,加载因子赋默认值0.75。
底层的Entry[]在第一次put的时候初始化
// 空参构造
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
// 指定数组初始化长度
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 指定数组初始化长度与默认负载因子
public HashMap(int initialCapacity, float loadFactor) {
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();
}
// 根据已有的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);
}
三:put方法
/**
* 扰动方法,传入key值,获取key值对应的一个hash值
* 为了减少hash冲突,所以尽量会复杂一些
*/
final int hash(Object k) {
// 取hash种子,参与hash计算,增加散列度
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 官方解释:这个函数确保在每个位元位置上仅以常数倍数存在差异的哈希值有一个有限的冲突数(在默认负载因子下约为8)。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 初始化数组,会对数组大小进行处理,确保是2的次幂
*/
private void inflateTable(int toSize) {
// 找到一个大于等于传过来的值,且为2的次幂的数,比如传过来10 返回16 传过来 17 返回32
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 在这里初始化Entry数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
/**
* 调用put()方法去往HashMap中保存值,头插法
* 返回值:替换原有值,会返回旧值,新增则返回null
*/
public V put(K key, V value) {
// 如果是空数组(第一次put),初始化数组
if (table == EMPTY_TABLE) {
//对数组膨胀,即初始化
inflateTable(threshold);
}
// 可以存储null键
if (key == null)
// 放置在底层数组下标为0的位置上
return putForNullKey(value);
// 对key进行hash计算,得到一个hash值
int hash = hash(key);
// 通过hash值与数组的长度-1(即掩码)进行与运算,获取到该值应该放在哪个数组的哪个位置
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;
}
/**
* 往链表上新增一个元素,先判断是否需要扩容
* 如果大于等于扩容阈值并且数组对应下标位置不为空,则扩容2倍
* 插入新元素
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
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);
}
/**
* 取出原数组对应值,新创建一个Entry,next属性为原数组对应值
* 将新Entry放到数组里
*/
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++;
}
总结一下put流程:
- 第一次put值时,初始化底层Entry数组,默认长度为16,负载因子为0.75
- 判断key是否是null,如果是null,单独处理,数组下标为0
- 计算出put值的hash值,经过掩码的位运算,获取到要保存的数组下标index
- 查出该下标对应的数组的Entry值,如果为空,则直接将put的值保存在该位置
- 遍历该数组下标对应的链表,如果找到相等的值则替换并返回旧值
- 如果未找到相等元素,即新增
- 新增时先判断是否需要扩容,如果需要扩容,则扩容并转移数据
- 将新增的元素,采用头插法,插入到数组对应坐标中
四:resize()扩容
扩容:扩容2倍,头插法
/**
* 扩容方法,newCapacity是已经乘以2以后的大小了
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
// 如果旧数组已经达到了最大容量,则不再进行扩容
threshold = Integer.MAX_VALUE;
return;
}
// new一个新的Entry数组
Entry[] newTable = new Entry[newCapacity];
// 将原数组的值转移到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 进行数组元素的转移
* 遍历原数组,hash值根据标志位判断是否需要重新计算(基本上不会重新计算),重新计算数组下标
* 将链表上的元素放到新数组中,头插法
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 这一步就是头插法
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
resize()方法总结:
- 扩容时首先判断原数组大小是否已经超过最大值了,超过就直接返回,不进行扩容操作
- 生成一个新的数组
- 将原数组的元素重新计算在新数组中的下标,转移到新数组中
- 转移的过程中,采用头插法,在多线程的情况下,有可能会形成一个循环链表
- 循环链表在下一次put/get时,有可能产生死锁现象
六:get方法
/**
* 先判断key是否是null,如果是null则单独处理,从数组下标为0的位置遍历取值
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key对应的Entry值
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
/**
*
*/
final Entry<K,V> getEntry(Object key) {
// 如果map的大小为0,说明没有值,直接返回null
if (size == 0) {
return null;
}
//计算hash值,然后计算对应下标,获取到该bucket的链表
// 循环遍历该链表,找到对应的值,找不到返回null
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;
}
get方法总结:
- 先判断key是否为null,为null单独处理,返回对应的值
- 再判断map大小是否为0,为0直接返回null
- 计算key对应的hash值,获取对应数组的下标的那个链表
- 遍历链表取值,取到直接返回,取不到返回null。
结语:还在学习过程中,做一个学习记录,如有不对的地方,欢迎批评指正。