主要方法解析
package cn.iyhome.hashmap;
import java.io.Serializable;
import java.util.*;
public class MyHashMap<K, V> extends AbstractMap<K, V>
implements Map<K, V>, Cloneable, Serializable {
//(1)初始化变量
//初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//装载因子
final float loadFactor;
//下次扩容阈值
int threshold;
//修改次数
transient int modCount;
//空表
static final Map.Entry<?, ?>[] EMPTY_TABLE = {};
//数组表,大小总是power of two
transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
//?
private transient Set<Map.Entry<K,V>> entrySet = null;
//map大小
transient int size;
//(2)构造方法
public MyHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity, float loadFactor) {
//初始值小于0:抛非法参数异常
if (initialCapacity < 0)
throw new IllegalArgumentException("...");
//初始值大于最大值,令初始值为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//装载因子异常小于0或者不是浮点数,抛非法参数异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("...");
//初始化参数装载因子
this.loadFactor = loadFactor;
//tableSizeFor:Returns a power of two size for the given target capacity.
//jdk1.8 -> this.threshold = tableSizeFor(initialCapacity);
this.threshold = initialCapacity;
}
//(3)jdk1.7 inflateTable方法
/**
* 初始化数组表
*
* @param toSize:默认值大小或用户在构造函数中设定的初始大小
*/
private void inflateTable(int toSize) {
//Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化大小
table = new Entry[capacity];
}
/**
* Find a power of 2 >= toSize
*
* @param number 默认值大小或用户在构造函数中设定的初始大小
* @return 修正后的值
*/
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//(4)jdk1.7 put方法
public V put(K key, V value) {
//如果数组为空
if (table == EMPTY_TABLE)
//初始化table数组
inflateTable(threshold);
//如果key为null
if (key == null)
return putForNullKey(value);
//对key取hash值 //hash():hashSeed ^=k.hashcode(),再对hashSeed做了四次异或和右移4位运算,保证hash值足够分散,减少碰撞
int hash = hash(key);
//indexFor()方法求出hash值对应的数组下标
int index = indexFor(hash, table.length);
//table[index]:链表的头结点;遍历链表
for (Entry<K, V> e = table[index]; e != null; e = e.next) {
Object k;
//如果key值重复,则覆盖value,返回旧value
if (hash == hash(key) && (((k = e.key) == key) || (key.equals(k)))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
//key不重复
}
modCount++;
addEntry(hash, key, value, index);
return null;
}
//(5)自定义hash方法
final int hash(Object k) {
//源码中:令hashSeed=k.hashcode(),再对hashSeed做了四次右移和异或运算,让高位也参与了运算,保证hash值足够分散,减少碰撞
int h = k.hashCode() % table.length;
return h;
}
//(6)jdk1.7 indexFor方法
/**
* 返回hash值h对应的数组下标
*
* @param h hash()返回的哈希值
* @param length 数组表的长度
* @return
*/
static int indexFor(int h, int length) {
/*
* 1.为什么要使用 h & (length-1) ?
* param: hash & length
* 假设:hash = 85 length = 16
* DEC: 85 & 16 = 16
* BIN: 0101 0101 hash
* & 0001 0000 length
* = 0001 0000 16
* 此时,如果hash的低4位无论如何变,结果都是 0001 0000,因为length的低4位全是0,这种情况下计算出的数组下标超出了数组表的大小范围
* 如果将length-1 即 length(15) = 0000 1111,此时
* BIN: 0101 0101 hash
* &0000 1111 length
* =0000 0101 5
* 最终的值随着hash的低4位变化而变化,而hash的低4位取值范围为 0000 - 1111 即 0-15,正符合数组表的下标长度
* 所以:这就是为什么table无论是在初始化或者扩容的时候,自始至终都是2的次方大小
* 2.为什么不用 % 取余来做? & 与操作效率高
* */
return h & (length - 1);
}
//(7)jdk1.7 putForNullKey方法
/**
* @param value 值
* @return oldValue
*/
private V putForNullKey(V value) {
//遍历table[0]为头结点的链表
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++;
addEntry(0, null, value, 0);
return null;
}
/**
* 添加一个新节点到对应的数组下标所在的链表,并在达到条件的时候进行扩容
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果大小超出阈值且要添加的位置已经存在值,则进行2倍扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
//key是否为null,null则数组下标为0,否则取hash
hash = (null != key) ? hash(key) : 0;
//再根据hash & length-1 ,获取到数组下标
bucketIndex = indexFor(hash, table.length);
}
//添加节点位头结点
createEntry(hash, key, value, bucketIndex);
}
/**
* 达到扩容条件后,对map中的key重新做hash取值
* 如果当前大小已经最大值,将不再扩容,但会将threshold的值设置为Integer.MAX_VALUE 1<<32 -1
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity 必须是2的次方且大于之前的大小,除非已经是最大值1<<30).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//transfer(newTable, initHashSeedAsNeeded(newCapacity));
transfer(newTable);
table = newTable;
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 将扩容前的节点复制到新集合
*
* @param newTable
*/
private void transfer(Entry[] newTable) {
int newCapacity = newTable.length;
//遍历数组
for (Entry<K, V> entry : table) {
//遍历链表
while (null != entry) {
//存储好entry的下一个节点
Entry<K, V> next = entry.next;
//重新获取hash值
entry.hash = null == entry.key ? 0 : hash(entry.key);
//根据新hash值计算在newTable的下标
int index = indexFor(entry.hash, newCapacity);
//链表尾插法
//如果newTable[i] == null,则直接将entry值覆盖newTable[i]
//如果 != null,则先将entry.next指向已经存在的值(即newTable[i],即链表头),然后将新节点置为链表头
entry.next = newTable[index];
newTable[index] = entry;
//将entry的next继续遍历
entry = next;
}
}
}
/**
* 删除元素
*
* @param key
* @return
*/
public V remove(Object key) {
Entry<K, V> e = removeEntryForKey(key);
return null == e ? null : e.value;
}
/**
* 根据key移除Entry节点
*
* @param key
* @return
*/
private Entry<K, V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//根据key获取数组下标
int hash = null == key ? 0 : hash(key);
int index = indexFor(hash, table.length);
//设置节点,使用链表的删除方法(指针指向)
Entry<K, V> prev = table[index];
Entry<K, V> entry = prev;
while (null != entry) {
Entry<K, V> next = entry.next;
Object k;
//找到key对应的节点entry
if (entry.hash == hash && ((k = entry.key) == key || (key.equals(k)))) {
modCount++;
size--;
//如果prev==entry,说明是第一个节点即头结点
if (prev == entry)
//直接将第二个节点作为头结点即可
table[index] = next;
else
//删除中间节点,将相对的第三个节点作为相对的第一个节点的下一个节点
prev.next = next;
entry.recordAccess(this);
return entry;
}
//如果遍历的第N个节点与key不匹配,则继续遍历链表
//将当前节点作为前一个节点
prev = entry;
//将下一个节点置为当前结点,继续遍历
entry = next;
}
//若null == entry
return entry;
}
// These methods are used when serializing HashSets
int capacity() {
return table.length;
}
float loadFactor() {
return loadFactor;
}
/**
* 清空集合
*/
public void clear() {
modCount++;
//将数组所有对象引用置为null
Arrays.fill(table, null);
size = 0;
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return null;
}
/*public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}*/
/**
* 创建一个节点
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
//记录链表头结点
Entry<K, V> e = table[bucketIndex];
//将头结点作为新节点的下一个节点,并将新节点放置在数组上
table[bucketIndex] = new Entry<>(hash, key, value, e);
//大小+1
size++;
}
/**
* 返回大小
*
* @return
*/
public int size() {
return size;
}
/**
* 判断集合是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* getValue
*
* @param key
* @return key对应的Value
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K, V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
/**
* 根据key获取Entry对象
*
* @param key
* @return
*/
final Entry<K, V> getEntry(Object key) {
if (size == 0) {
return null;
}
//取key的hash
int hash = null == key ? 0 : hash(key);
//遍历查找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 || (k != null && key.equals(k))))
return e;
}
return null;
}
/**
* 是否包含key的value
*
* @param key
* @return
*/
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
/**
* get value when the key is null
*
* @return entry.value
*/
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
static class Entry<K, V> implements Map.Entry<K, V> {
final K key;
V value;
Entry<K, V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K, V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(MyHashMap<K, V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(MyHashMap<K, V> m) {
}
}
}
常见问题
1.HashMap底层的数据结构
JDK1.7以前HashMap底层是 数组+链表,jdk1.8使用的 数组+链表+红黑树
2.数组元素的数据类型
数组的类型是 HashMap.Entry<K,V> implement Map.Entry<K,V>,
HashMap.Entry<K,V>主要是维护K,V映射关系,Map.Entry维护了<K,V,next>
3.HashMap中数组的长度
数组的长度一直为2的次方.
如果在构造函数中定义的长度不是2的次方?
jdk1.7中,会在put方法中,如果数组是空,会调用inflateTable方法中的roundUpToPowerOf2()方法修正数组的长度,并计算下次扩容的阈值(数组长度*装载因子);
jdk1.8中,是在构造方法中就调用tableSizeFor()方法,将长度修正为2的次方
4.HashMap什么时候扩容?
put数据的时候,调用addEntry方法.当达到阈值threshold&&table[index]!=null的时候
源码为:if((size>=threshold)&&(null!=talble[indext]))
5.put元素的时候,index是如何计算的?
(1)如果key是null,固定位置index=0(注意:并不是index=0的位置全是key=null的数据)\
(2)第一步,通过hash(key)[四次异或和右移运算]取的一个较为分散的hash值;
第二步,通过hash & length-1计算出index的值
为何是hash & length-1?第一部分源码分析有讲解
6.key重复的判断标准
key.hash == hash && (key.equal(k)||(k = e.key) == key)
即hash值相同和(key的地址相同或者equals相等)
7.集合扩容的是什么?
是数组+链表中,数组的大小.所以源码中初始值2<<4也是数组的大小,
而不是集合中所有元素的个数,即size
8.JDK1.7HashMap扩容死循环问题?
在多线程并发访问触发扩容后,可能会导致entry和next entry相互引用,导致死循环
9.JDK1.8 HashMap什么时候扩容?
第一种:链表的个数达到8个,并且table.length<64,那么会扩容
第二种:size >= threshold并且talbe[index]!=null
10.HashMap插入元素时,1.7和1.8有什么区别?
JDK1.7是头插法(所以会导致元素顺序颠倒),JDK1.8是尾插法
11.JDK.18中HashMap什么时候用到红黑树?
链表超过8个且table.length>64的时候树化,当树的节点小于6个,就会反树化。
12.如何解决HashMap的并发扩容导致的死循环问题?
1.整个方法加锁,这也是hashtable采用的方式
2.分段锁(segment),currentHashMap采用的方式.
并发级别默认16,也就是段数. 每个段就是一个Segment对象(每个Segment就是一个小的加了锁的HashMap),多个Segment就组成了一个数组.
所以currentHashMap的put方法分为两步:第一步,通过key.hashCode找到Segment[index],再到小的HashMap中找到index
13.ConcurrentHash是如何控制Segment.size一定是2的次方的?
构造函数中有while循环ssize<<=1操作
14.JDK1.8扩容的迁移机制?
分为链表迁移和红黑树迁移.链表迁移使用了4个指针代替了rehash