HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap的结构如下:
我们可以看到HashMap的结构主要分为两大部分:左侧的table和右侧的链表,下面重点分析HashMap的源码。
1.常量
/**
* 默认容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
*最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 存储元素的数组
*/
transient Entry<K,V>[] table;
/**
* 大小 */
transient int size;
/**
* 临界值 (默认容量 * 加载因子).
* @serial
*/
int threshold;
/**
* 加载因子
*/
final float loadFactor;
/**
* 被修改的次数 */
transient int modCount;
2.常用的构造方法
/**
* 指定初始容量和加载因子
*/
public HashMap(int initialCapacity, float loadFactor)
/**
* 指定初始容量,默认加载因子为0.75f
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
*默认的容量16和默认的加载因子0.75f
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
3.常用方法
(1)、final int hash(Object k),此方法用来计算key的哈希值,由方法最后的几行可以看到,为了保证key的均匀散列,并没有使用hashCode%length的方法,因为移位运算符不仅可以更均匀的散列而且匀速速度也比hashCode%length更快。
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
(2)、public V get(Object key)
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
我们看到,get方法首先判断key是否为null,如果为null,则调用getForNullKey(),否则调用getEntry(key)方法获取value,让我们来继续分析getForNullKey的源码:
private V getForNullKey()
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
可见:循环遍历table[0]的那个链表,直到找到key==null的Entry,否则返回null;为什么是table[0]呢?因为在put的时候如果key==null,直接将entry存储在table[0]的位置,我们在后面分析put方法。接下来再分析getEntry(key)方法:
final Entry<K,V> getEntry(Object key) {
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;
}
/**
* 通过key的Hash值计算所在的table下标
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
通过key的Hash值计算Entry所在table的小标,请注意计算下标的方法通过indexFor()方法得到,然后遍历Entry,直到找到hash和equals都相等的Entry后返回,否则返回null;
好了,get方法基本就是这样,接下来我们一起来分析put方法。
(3)、 public V put(K key, V value)
public V put(K key, V value) {
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;
}
put方法的大致流程是:
①、首先判断key是否为null,如果为null,调用putForNullKey(value)方法,请大家注意,putForNullKey和上面的getForNullKey的逻辑是一一对应的哦。
②、计算key的Hash值,通过indexFor()方法定位到元素存储在table的位置table[i]。
③、循环遍历table[i],如果新值和旧值相等,覆盖旧值后返回旧值
④、modCount++,操作次数+1,调用addEntry将新值插入到链表。
让我们来分析putForNullKey(value)方法源码:
private V putForNullKey(V 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++;
addEntry(0, null, value, 0);
return null;
}
请看,这里又是table[0],和getForNullKey中的table[0]对称,遍历-->新值覆盖旧值-->返回旧值-->操作数+1-->插入新元素,很容易理解。
接下来重点来了,让我们来分析addEntry方法中的一系列逻辑:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
首先,threshold=capacity * load factor,也就是临界值=容量*加载因子=16*0.75f, map中使用量超过threshold,会扩容为原来的2倍。resize是一个非常复杂的过程,涉及到rehash等,后面我在介绍,现在咱们重点看addEntry()。bucketIndex是元素存储在table的下标,也就是将元素存储在table[bucketIndex]。最后调用createEntry将新元素存储在HashMap中,createEntry的源码如下:
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++;
}
通过new Entry(hash,key,value,e),将新元素插到table[bucketIndex]中,我们来看Entry的构造方法:
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
重点在next = n这行,看到到插入元素到链表中使用的头插法,不用尾插的目的估计是为了节省遍历链表的开销吧。
好了,put方法就是这样,其实也特别好理解,接下来我们来分析Entry这类。
4.Entry内部类
HashMap有一个变量:transient Entry<K,V>[] table,可以看到table是一个Entry的数组,并且Entry本身是一个链表的一个元素。Entry<K,V> implements Map.Entry<K,V>,而Map.Entry是一个接口。Entry包含四个元素,final K key; V value; Entry<K,V> next;int hash;hash是这个元素的hash值,其余的三个元素就不解释了。Entry类的方法基本上都很简单,大家可以通过阅读源码来理解,我重点解释一下equals和hashCode这两个方法,源码如下:
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;
}
比较连个Entry是否相等就是通过if中的条件,比较容易理解,hashCode源码:
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
最后,重点分析rehash方法,在分析rehash时,我先解释一下什么是rehash:当我们的HashMap的使用量超过了threshold=capacity * load factor,也就是临界值=容量*加载因子=16*0.75f,就会发生rehash,将容量扩大为原来的两倍,然后所有的元素根据新的hash规则重新散列到不同的table[i]中。但是rehash有很大的性能消耗,所以如果我们在使用HashMap时能预测到元素的个数,最好在构造时就指定HashMap的大小。rehash的源码如下:
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];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
Entry[] newTable = new Entry[newCapacity],常见一个新的table,很消耗内存的!!!前面容易理解,重点看transfer方法,这个方法才是将元素重新散列的方法,源码如下:
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;
}
}
}
循环遍历table(旧数组),计算出newTable的下标,将旧元素e存储到newTable中,这里有一个细节要注意,在我讲put方法时提到过,插入元素的方法时头插法,就是新的元素被添加到链表的头部,但我们通过transfer方法可分析出:在rehash的时候,之前在头部的元素会先进行rehash,在尾部的元素会最后rehash,所以当rehash结束后,之前在头部的元素会沉到尾部,之前在尾部的元素会上升到头部。
好了,到此为止,HashMap常用的方法分析完了,感谢大家阅读,如果有错误的地方欢迎大家指出,谢谢大家。