hashmap中的几个重要属性,及其构造方法
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
//hashmap默认长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//hashmap最大长度2的30次幂
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子,跟扩容有关
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//hashmap初始为空数组,用于后面的初始化数组提供判断
static final Entry<?,?>[] EMPTY_TABLE = {};
//hashmap存放Entry引用的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//hashmap的总元素个数
transient int size;
//元素个数达到此值就可能会进行扩容
int threshold;
//加载因子
final float loadFactor;
//hashmap每次修改,该值会加一
transient int modCount;
hashmap中的数组存放的Entry引用数组,该Entry的属性如下所示
static class Entry<K,V> implements Map.Entry<K,V> {
//节点的key值
final K key;
//节点的value值
V value;
//同一个桶中形成单向链表。指向下一个节点
Entry<K,V> next;
//当前节点的hash值
int hash;
hashmap中的几个重要构造方法如下所示
//有参构造方法,可以手动传入容量大小和加载因子
public HashMap(int initialCapacity, float loadFactor) {
//容量值小于0直接抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//容量值大于最大值,默认为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子需要大于0且需要为数值类型,否则抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//加载因子赋值
this.loadFactor = loadFactor;
//赋值容量,后面会做处理重新计算数组容量以及threshold值
threshold = initialCapacity;
//hashmap中没有实现该方法,用于LinkedHashMap
init();
}
//手动设置容量值,加载因子默认0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//默认构造方法,会调用有参构造方法,传的默认容量和默认加载因子
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//可以传入一个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);
}
hashmap的put方法源码分析
//添加元素
public V put(K key, V value) {
//初始时hashmap的table数组赋值为EMPTY_TABLE,第一次要进行初始化数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//对空值Key进行了处理,说明hashmap支持存放空值
if (key == null)
return putForNullKey(value);
//调用hash函数算出hash值
int hash = hash(key);
//根据数组长度以及hash值算出对应的桶位置
int i = indexFor(hash, table.length);
//先进行遍历,如果找到了相同的key,则直接进行值覆盖,返回旧value值
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;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//找到大于等于toSize的最小2的幂次方数
int capacity = roundUpToPowerOf2(toSize);
//重新计算阈值,大于等于该值则会进行扩容的判断
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化hashmap的Entry引用数组
table = new Entry[capacity];
//初始化hash种子
initHashSeedAsNeeded(capacity);
}
//插入的key为空。做特殊处理
private V putForNullKey(V value) {
//key为空的元素直接放在桶的第一个位置,遍历第一个桶的链表。找到则进行值覆盖,并把旧value返回
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//放在第一个位置。默认hash值为0
addEntry(0, null, value, 0);
return null;
}
//计算元素下标
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//用hash值与数组长度减一进行与运算,这里也是数组的长度为什么需要是2的幂次方数
//举个例子
h = 0001 1010
length = 0001 0000
与运算
0001 1010
0000 1111
0000 1010 ==> 1010在0000-1111之间
也就是保证我们的与运算结果在0-length-1长度之间
return h & (length-1);
}
//算出大于等于number的最小二的幂次方数
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
//Integer.highestOneBit算出的是小于等于传入的值的最大二次幂数,
//然后再左移一位则变成大于等于传入值的最小二的幂次方数,
//但是存在特殊情况,所以要减一,如果不减一,
//比如16.先翻倍成32.利用Integer.highestOneBit算出的就是32,会出错,所以需要先减一。
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//因为int型为32位,需要进行32次移位,
//该方法利用把i的最高位1后面的0全部变成1,
//然后减去i向右移动一位。把i最高位1后面的全改成0.
//则就是我们想要的答案
//比如拿八位做例子,实际上是32位
17 0001 0001
处理后
0001 1111
减一
0001 1111
0000 1111
0001 0000 ==> 16
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
//添加元素操作
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果当前长度大于等于threshold阈值并且当前的桶位置不为空,再进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容为原来的两倍
resize(2 * table.length);
//扩容后重新算hash值
hash = (null != key) ? hash(key) : 0;
//扩容后重新算桶下标
bucketIndex = indexFor(hash, table.length);
}
//进行真正的添加
createEntry(hash, key, value, bucketIndex);
}
//利用头插法进行元素的添加
void createEntry(int hash, K key, V value, int bucketIndex) {
//当前桶位置的第一个元素
Entry<K,V> e = table[bucketIndex];
//把待插入节点的next置为桶位置的第一个元素,再把待插入节点赋值给当前桶下标.当前桶下标存放的实际上是引用,真正的元素对象在堆中。
table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素总数加一
size++;
}
添加方法小结
因为初始时hashmap的Entry数组赋值为EMPTY_TABLE,所以第一次插入元素时会进行数组的初始化,然后再进行添加,如果添加的值key为null,会进行特殊处理的添加。key不为空然后调用hash()方法进行hash值计算,根据hash值与数组长度进行桶下标的确认,确认位置后进行当前桶位置的链表遍历,如果中途遍历到与要添加的元素key值相同,则直接进行覆盖并把旧的value值返回。然后把修改次数加一。遍历到数组尾部还没有找到则进行添加操作,会先进行一次是否需要扩容的判断,如果需要扩容,则进行两倍的扩容,再重新计算桶下标并添加。
hashmap的扩容源码分析
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//扩容条件有两个,要同时满足当前容量大小大于等于阈值,并且当前要插入桶位置的第一个元素不为空
//扩容方法
void resize(int newCapacity) {
//获得旧tableEntry数组
Entry[] oldTable = table;
//获得旧数组长度
int oldCapacity = oldTable.length;
//如果容量已经是最大值了。则不能再进行扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个长度为旧Entry数组两倍的新table数组
Entry[] newTable = new Entry[newCapacity];
//进行元素的转移
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//把hashmap的table属性指向新的数组
table = newTable;
//重新计算阈值用于下一次扩容
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//转移元素
void transfer(Entry[] newTable, boolean rehash) {
//新数组长度
int newCapacity = newTable.length;
//进行旧Table的遍历一个个迁移
//如果多线程操作,可能会出现循环链表,线程不安全
//至于为什么要进行一个个迁移而不是直接将第一个元素直接迁移,是为了让每个位置
//的链表尽可能的散列到不同的位置
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
//重新hash,一般不需要。
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;
}
}
}
扩容方法小结
首先进行扩容的条件有两个,一个是需要当前hashmap的总元素数量大于等于阈值且当前插入的桶位置第一个元素不为空,两则缺一不进行扩容。满足条件后先创建一个长度为原来Entry数组的两倍,然后再遍历每个桶的每个链表进行头插到新的数组中。扩容时如果有多个线程同时进行扩容,可能会出现循环链表的情况,需要代码中进行线程安全处理。
hashmap的get方法源码分析
public V get(Object key) {
//如果key为空,进行特殊查询
if (key == null)
return getForNullKey();
//查询元素
Entry<K,V> entry = getEntry(key);
//进行返回
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//算出桶下标
int hash = (key == null) ? 0 : hash(key);
//遍历该下标的链表,找到则进行返回,否则返回null
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;
}
获取方法小结
该方法比较简单,如果传入的key为空,则直接进行特殊查询,否则根据key算出桶下标,遍历该下标的链表所有元素,遍历中找到则直接进行返回,否则返回null值