先看类图结构:
HashMap
HashMap 实现了Map接口,扩展了AbstractMap抽象类
1.成员变量
// HashMap的默认初始容量,即hash表桶的初始个数,即数组初始长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap的最大容量,即hash表桶的最大个数,即数组最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
// HashMap的默认负载因子
// threshold = 负载因子 * 容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 代表空数组
static final Entry<?,?>[] EMPTY_TABLE = {};
// HashMap真正用于存放元素的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// hash表元素总个数
transient int size;
// 临界值
int threshold;
// 负载因子
final float loadFactor;
解释:
- 在HashMap在Entry数组长度大于threshold时,如果put()元素时发送hash冲突,则会触发扩容,即将HashMap的Entry数组的capacity扩大为原来的两倍。
- 默认初始容量一定是2的指数倍,就算指定的初始容量不是2的倍数,也会向上取到大于它的2的指数倍的数中的最小数(比如7就变成8,13就变成16…)。这是等会儿由于根据元素的hash计算放入元素的index时,运用了取模元算,同时想要提高计算机效率。
2.HashMap方法暂不罗列
HashMap内部类Entry
1.成员变量
final K key;
V value;
Entry<K,V> next;
int hash;
2.Entry方法暂不罗列
正片开始
一、新建HashMap
- Test.java (测试类)
//新建HashMap
HashMap map = new HashMap(); < — — — — — a.进去
Object obj = map.put("java", "1.7");
- HashMap
// 构造函数1
public HashMap() {
// DEFAULT_INITIAL_CAPACITY 等于 1 << 4,也就是16
// DEFAULT_LOAD_FACTOR 等于0.75f
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); < — — — — — b.进去
}
- HashMap
// 构造函数2
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);
// 简单地为loadFactor和threshold属性赋值
this.loadFactor = loadFactor;
threshold = initialCapacity;
// 空方法,啥也不做
init();
}
二、调用put()方法
- Test.java (测试类)
//新建HashMap
HashMap map = new HashMap();
Object obj = map.put("java", "1.7"); < — — — — — a.进去
- HashMap
public V put(K key, V value) {
// 由于刚刚初始化,table == EMPTY_TABLE,进入if
if (table == EMPTY_TABLE) {
// threshold == DEFAULT_INITIAL_CAPACITY == 16
inflateTable(threshold); < — — — — — b.进去
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
- HashMap
// 初次扩容,参数toSize == 16
private void inflateTable(int toSize) {
// 把传入参数向上取为2的指数倍最小的数
int capacity = roundUpToPowerOf2(toSize); < — — — — — c.进去
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
- HashMap
// 该函数专门把传入的数变成其向上取的2的指数倍的最小的数
// 参数number == 16
private static int roundUpToPowerOf2(int number) {
// 先判断【number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1)】
// 有以下3中可能:
// 1️⃣ 如果number小于MAXIMUM_CAPACITY且大于1,那就得到(number > 1)
// 1.2 即得到true; 所以返回Integer.highestOneBit((number - 1) << 1)
// 1.3 该表达数实现了向上取的2的指数倍的最小的数
// 1.4 number - 1是为了处理4、8、16这类已经是2的指数倍的数
// 2️⃣ 如果number>= MAXIMUM_CAPACITY,那就得到MAXIMUM_CAPACITY
// 2.2 也会得到true;所以返回Integer.highestOneBit((number - 1) << 1) 同上
// 3️⃣ 如果number<=1,那就得到(number > 1)
// 3.2 所以得到false;所以返回1
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; < — — — — — d.返回
}
- HashMap
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
// 现在capacity == 16
// threshold = min(16*0.75,MAXIMUM_CAPACITY + 1)
// threshold = 12
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 对数组table赋值,是一个容量为16的元素为Entry类型的数组
table = new Entry[capacity];
//不管这句
initHashSeedAsNeeded(capacity);
// 初次扩容完毕,返回
}
- HashMap
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果key == null,那么执行putForNullKey。此处跳过
// putForNullKey()方法很简单
if (key == null)
return putForNullKey(value);
// 根据key计算该元素的hash
int hash = hash(key); < — — — — — e.进入
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;
}
- HashMap
// 传入的参数k就是key
// 此函数就是计算出key的hash值,保证充分“打散”
final int hash(Object k) {
// h = hashSeed = 0;
int h = hashSeed;
// 如果h不等于0且key是String类型及其子类,此处跳过
if (0 != h && k instanceof String) {
// 返回一种hash算法
return sun.misc.Hashing.stringHash32((String) k);
}
// h 和 key的hashCode做按位异或
h ^= k.hashCode();
// h右移20位,以及h右移12位,相互异或
h ^= (h >>> 20) ^ (h >>> 12);
// h再右移,再异或,返回
return h ^ (h >>> 7) ^ (h >>> 4); < — — — — — f.返回
}
- HashMap
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 根据元素的hash值,和数组的长度得到该元素的index
int i = indexFor(hash, table.length); < — — — — — g.进入
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;
}
- HashMap
// 此函数用于得到元素的index
// 参数h是hash值,lenght是16
static int indexFor(int h, int length) {
// h & (length-1) 等价于 h % length
// 相当于取模运算,根据余数得到index
// 由于之前规定了length必须是2的指数,所以length-1是全1的数,如1111
// 这样取模运算就转化成了按位与,加快了运算效率
return h & (length-1); < — — — — — h.返回index
}
补充:计算机本身不太擅长除法
- HashMap
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
// 根据index,找到数组上那个桶,遍历以它为首的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果遍历找到了和当前插入一样的元素(根据key判断),则更新它的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
// 返回即可
return oldValue;
}
}
modCount++;
// 如果遍历了全hash表都没有找到相同元素,则new一个该元素的Entry加进去
addEntry(hash, key, value, i); < — — — — — i.进入
return null;
}
- HashMap
// 该函数把新建的Entry加入到hash表中
// 参数hash为hash值;key就是key,value就是value,bucketIndex就是插入桶位置index
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果此时hash表元素个数>=threshold,且发生了hash冲突。此处暂时跳过,稍后分析
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 真正的执行把新建的Entry加入到hash表中
createEntry(hash, key, value, bucketIndex); < — — — — — j.进入
}
- HashMap
// 参数hash为hash值;key就是key,value就是value,bucketIndex就是插入
void createEntry(int hash, K key, V value, int bucketIndex) {
// 得到并取出数组现在index位置上的元素e
Entry<K,V> e = table[bucketIndex];
// 把数组index位置让给新元素Entry,该新元素Entry的next的指针指向刚刚的e
table[bucketIndex] = new Entry<>(hash, key, value, e);
// hash表元素个数+1
size++;
// 一路返回
}
结束
三、HashMap的扩容
- HashMap
上述put的过程中,第14步添加新Entry,假设此时满足了扩容条件,触发扩容,进入了if语句:
// 该函数把新建的Entry加入到hash表中
// 参数hash为hash值;key就是key,value就是value,bucketIndex就是插入桶位置index
void addEntry(int hash, K key, V value, int bucketIndex) {
// 此时hash表元素个数>=threshold,且发生了hash冲突
if ((size >= threshold) && (null != table[bucketIndex])) {
// 指向扩容函数,参数为原容量的两倍:32
resize(2 * table.length); < — — — — — a.进入
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
- HashMap
// 参数newCapacity = 32
void resize(int newCapacity) {
// 用oldTable承接现在的hash表数组
Entry[] oldTable = table;
// 得到现在的容量
int oldCapacity = oldTable.length;
// 跳过
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建新的hash表数组,数组长度为32
Entry[] newTable = new Entry[newCapacity];
// 新表换旧表,元素搬家
transfer(newTable, initHashSeedAsNeeded(newCapacity)); < — — — — — b.进入
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
解释:initHashSeedAsNeeded(newCapacity)暂不明白如何触发,这里不影响
- HashMap
// 该函数负责具体的扩容,元素的搬家
// 参数:新的空hash表,rehash标志
void transfer(Entry[] newTable, boolean rehash) {
// newCapacity = 32
int newCapacity = newTable.length;
// 对于旧表里的每一个桶Entry元素,遍历
for (Entry<K,V> e : table) {
// 当该桶不为空,遍历该桶下面的链表
while(null != e) {
// 下一个Entry
Entry<K,V> next = e.next;
// 如果重hash
if (rehash) {
// 如果有key值 == null,e.hash = 0
// 如果key值 != null,重hash运算:e.hash = hash(e.key)
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新计算该Entry元素的index
int i = indexFor(e.hash, newCapacity);
// 下面两步:该Entry加入到i桶的链表中,头插法
e.next = newTable[i];
newTable[i] = e;
// 迭代下一步
e = next;
}
}
// 一路返回
}
扩容分析结束
简单说明问题:
- JDK1.7的HashMap在方面容易出问题:在于他元素搬家是,可能会造成原来一个链上的元素顺序倒置,在多线程环境下扩容可能会倒置环形链表形成死循环。这时CPU100%
- 由于可以构造大量的String的,使它们具有相同hashcode,从而大量的key在同一个桶内,hashmap退化成链表。早先的tomcat是用hashmap接受http线程的请求参数,此举会让tomcat性能急剧下降,形成DDoS攻击。