HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。 HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。 HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,大小为16。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度,大小为0.75。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
HashMap底层使用EMPTY_TABLE数组来存储key/value
//HashMap使用Entry类型的数组存储
static final Entry,?>[] EMPTY_TABLE = {};
transient Entry[] table = (Entry[]) EMPTY_TABLE;
Entry类结构,当存在hash冲突时,entry的next变量指向冲突链表的下一entry
static class Entry implements Map.Entry {
final K key;
V value;
Entry next; //指向链表的下一个entry节点
int hash; //此entry的hash值
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;
}
//修改当前entry的value,返回旧的value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断两个entry是否相等
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();
//比较key
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
//比较value
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
//计算hash值--key和value的hash值的二进制异或作为entry的hash值
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
void recordAccess(HashMap m) {
}
void recordRemoval(HashMap m) {
}
}
//数组容量的初始大小为16,1左移4位
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//数组的最大容量为1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子为0.75,当数组容量*加载因子小于Entry的个数(size)时,扩大数组的容量为当前数组大小的两倍,全部entry重新进行Hash值计算进行散列
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当前HashMap存储的entry个数
transient int size;
(1)put方法
//put方法
public V put(K key, V value) {
//此处相当于懒加载,当数组未创建时,进行数组的初始化,大小为16
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//当key为null时,放在数组的第一位,即Table[0]
if (key == null)
return putForNullKey(value);
//根据key计算hash值
int hash = hash(key);
//根据hash值和数组的长度计算entry需要放置的数组下标,方法见(2)
int i = indexFor(hash, table.length);
//循环遍历当前位置的链表,如果存在key相同的,则用新的value替换掉旧的value
for (Entry 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++;
//增加元素,方法见(3)
addEntry(hash, key, value, i);
return null;
}
(2)indexFor 方法,计算存放的下标
/**
* @param h Hash值
* @param length HashMap底层数组长度
* index的计算方式为二进制&运算,因为数组的下标为0-15,所以用length-1进行运算;
* 其次length的数值为16为基础,逐次2倍扩大。以16为例,二进制为10000,当10000与其它数字进行与运算时,产生的值只有0或者16,会产生大量的Hash冲突;当采用15时,1111和其它数值&计算结果会分布较为均匀。
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
(3)addEntry:增加元素,判断是否需要扩容
/**
* @param hash 经过key计算出来的hash
* @param key 插入的元素的key
* @param value 插入元素的value
* @param bucketIndex 插入数组的下标
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
// threshold = capacity * loadFactor,即数组长度 * 加载因子(0.75),作为阈值
if ((size >= threshold) && (null != table[bucketIndex])) {
//达到阈值,数组扩容为原来的2倍,方法见(4)
resize(2 * table.length);
//因为数组改变,重新进行计算hash值和数组索引
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//没有达到阈值,增加元素, 方法见(5)
createEntry(hash, key, value, bucketIndex);
}
(4)resize 方法:扩容,重新生成数组进行所有元素的散列
/**
* @param newCapacity 新数组的大小
*/
void resize(int newCapacity) {
//取到原数组
Entry[] oldTable = table;
//原数组长度
int oldCapacity = oldTable.length;
//最大长度
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新数组,长度为原数组的2倍
Entry[] newTable = new Entry[newCapacity];
//所有元素进行hash值和数组索引的计算,重新散列
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
(5)createEntry 方法:添加元素,并解决hash冲突
/**
* @param hash 经过key计算出来的hash
* @param key 插入的元素的key
* @param value 插入元素的value
* @param bucketIndex 插入数组的下标
* 插入元素,并解决hash冲突
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
//如果当前节点存在其它元素,则发生hash冲突,由新插入的元素顶掉原位置的元素,新元素的next指向被顶掉的旧元素,即新元素是从链表头部插入,e为链表的头元素
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素个数增加
size++;
}
(1)get方法
/**
* @param key 要获取元素的key
*/
public V get(Object key) {
//如果key为null,从存储key=null的位置获取元素,方法见(2)
if (key == null)
return getForNullKey();
//获取key,方法见(3)
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
(2)getForNullKey 方法:由put方法可知,key=null的元素都放在数组下标为0的位置
private V getForNullKey() {
//如果HashMap没有存储元素,返回null
if (size == 0) {
return null;
}
//取到数组下标为0的链表,遍历获取key为null元素返回
for (Entry e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
(3)getEntry 方法
final Entry getEntry(Object key) {
//如果HashMap没有存储元素,返回null
if (size == 0) {
return null;
}
//计算key的hash值
int hash = (key == null) ? 0 : hash(key);
//通过key的hash值,计算数组存储的下标,循环遍历下标位置存储的链表,返回key相等的元素
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;
}
由上所见,1.7解决hash冲突采用的是链表结构,而到了1.8,源码如下所示,插入元素时,首先判断插入的是否是树类型,如果不是,则判断冲突位置处的链表长度是否达到阈值(TREEIFY_THRESHOLD = 8),即链表长度达到8时,链表转为红黑树存储,未达到8时,则依旧采用链表结构。