HashMap声明
文中代码无特殊说明为Android-23,所以不同Android版本或者Java源码可能与文中不一样;如果不一样也不要大惊小怪。
HashMap类声明如下:
public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable{}
HashMap继承自AbstractMap,实现了Cloneable和Serialable接口,ABstractMap实现了Map接口,关于这一点没有什么特别之处;但是在Java的源码中我们会发现HashMap有如下声明:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{}
HashMap继承AbstractMap实现了Map接口,但是有一点比较奇怪的是,为什么AbstractMap已经实现了Map接口,HashMap还要实现Map接口,关于这一点我也是在网上找了一下原因,结果尴尬。在StackOverFlow上找到了这个问题:Why does LinkedHashSet extend HashSet and implement Set,负责集合这块开发的Josh Bloch说这是一个错误,其实类似的还有很多。就不纠结这个问题啦。可见貌似在这个版本下,把这个问题给修复了,其它Android版本的HashMap代码我还没有看。
添加元素
关于添加元素,HashMap提供了两个方法put,putAll方法,重点是put方法,其实putAll内部也是调用了put方法,OK,我们直接看put方法的实现即可:
public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
关于HashMap是否可以存放键值为空的键值对,上面代码已经给出了明确的答案,专门有个方法putValueForNullKey处理键为空的情况,我们就不在这里分析了;下面就是根据key获取一个hash值,这个值是根据key的hashCode获取的,后面再根据hash和数组table的长度获取一个index,这个index就是键值对要放的位置。我们可以看到求index的方法为hash & (tab.length - 1)这种算法取余是有条件的,那就是tab.length为2的n次方。因此HashMap的容量为2的n次幂,位运算的速度快,通过这种方法效率会更高。在我们创建HashMap实例的时候也会对传入的初始化容量做检验,保证容量是2的n次幂。
再看下面的循环,首先获取到数组中index位置的对象,如果为空就退出循环,继续下面的逻辑,如果不为空,就检查hash值和key的equals方法是否都一样,如果一样就进行覆盖,返回老的值;从这里我们可以看出,这种情况是对于键值对的更新。比如先开始put(“key”,”oldValue”);然后执行put(“key”,”newValue”);方法返回oldValue,存储newValue。
如果不是覆盖旧值,就是新增啦,会执行for循环下面的逻辑,判断容量是否需要扩容,通过doubleCapacity我们也能发现HashMap的容量扩容是2的倍数,保证容量是2的N次幂;如果扩容重新计算下index,最后调用addNewEntry方法
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
addNewEntry只有一行代码,极其简单,我们发现它把key和value封装成了一个HashMapEntry实例方法数组的table的index位置,我们来简单看下HashMapEntry类:
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
.....
}
HashMapEntry是HashMap的一个静态内部类,包含了四个属性:key(键),value(值),hash(根据键计算出来),next(下一个entity)
从上面的分析我们也能看出HashMap的存储结构为数组+链表的形式,如下图
查找元素
HashMap查找元素是通过get方法来实现的,传入一个key返回Value,如果没有找到则返回null,通过上面的图和添加元素的代码分析,我们大概也能猜出真个查找过程,第一步同样是获取index,第二个遍历链表比对key返回value
public V get(Object key) {
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
return e == null ? null : e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}
过程如我们上面分析一样;意外收获是对于key等于null的情况做了特殊处理,HashMap中有一个entryForNullKey专门储存储键为空的情况。其实添加元素中putValueForNullKey的过程就是创建entryForNullKey的过程。
HashMap扩容
我们上面在分析put方法时提到,当容量大于要扩容容量时会调用doubleCapacity进行扩容,我们来详细看下这个方法,了解下整个的扩容过程
private HashMapEntry<K, V>[] doubleCapacity() {
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity; j++) {
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
HashMap扩容大致可以分成两个步骤,第一步就是创建新的数组,对应for循环以上的代码;第二部遍历集合放入到新的数组中,其实我至今还有点不明白的就是在把元素放置到新数组中时为什么不直接调用put(key,vlaue)方法呢?而是直接通过位运算,重新计算出index,应该为了效率吧?!
构造函数
重点是两个参数的构造参数有点意思
public HashMap(int capacity, float loadFactor) {
this(capacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Load factor: " + loadFactor);
}
/*
* Note that this implementation ignores loadFactor; it always uses
* a load factor of 3/4. This simplifies the code and generally
* improves performance.
*/
}
这个构造函数第二个参数代表的是增长因子,其实这个方法中压根没用到loadFactor参数,只是做了校验而已。增长因子固定是0.75。