android-23/java/util/HashMap.java
public class HashMap< K, V > extends AbstractMap< K, V > implements Cloneable, Serializable
没有实现Collection接口
属性
最小容量
private static final int MINIMUM_CAPACITY = 4;
最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
加载因子
static final float DEFAULT_LOAD_FACTOR = .75F;
底层存储数组,是个Entry类型的数组,其实就是个hash表
transient HashMapEntry<K, V>[] table;
键值为null的存储对对象
transient HashMapEntry<K, V> entryForNullKey;
元素个数
transient int size;
扩容限值,通常为容量和加载因子的乘积
private transient int threshold;
private transient Set<K> keySet;
private transient Set<Entry<K, V>> entrySet;
private transient Collection<V> values;
Map接口
public interface Map<K,V> {
public static interface Entry<K,V> {
public boolean equals(Object object);
public K getKey();
public int hashCode();
public V setValue(V object);
};
public void clear();
public boolean containsKey(Object key);
public boolean containsValue(Object value);
public Set<Map.Entry<K,V>> entrySet();
public boolean equals(Object object);
public V get(Object key);
public int hashCode();
public boolean isEmpty();
public Set<K> keySet();
public V put(K key, V value);
public void putAll(Map<? extends K,? extends V> map);
public V remove(Object key);
public int size();
public Collection<V> values();
}
Map接口里面有内部定义了一个Entry< K,V >接口,定义了Map里面的存储元素结构。
元素对象
包括键、值、hash值、next指针域,因为HashMap解决hash冲突采用链地址法(不过java 8有了不同的方式,性能更高)
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> ne
this.key = key;
this.value = value;
this.hash = hash;
//头插法
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry<?, ?> e = (Entry<?, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
}
@Override public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
@Override public final String toString() {
return key + "=" + value;
}
}
添加
@Override public V put(K key, V value) {
if (key == null) {
//键为null时,调用此方法
return putValueForNullKey(value);
}
//对键进行hash
int hash = Collections.secondaryHash(key);
//table就是HashMapEntry<K,V>数组
HashMapEntry<K, V>[] tab = table;
//新元素在HashMapEntry<K,V>数组中的位置,由键的hash值和数组长度共同决定
int index = hash & (tab.length - 1);
//检测此下标上是否有元素,若有,遍历此处的链表(链表中的元素hash值都相同),覆盖键相同的值
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;
}
}
// Map里面没有键相同或键为null的元素
modCount++;
if (size++ > threshold) {
//元素个数大于扩容限度,2倍扩容
tab = doubleCapacity();
//用新的长度来计算下标
index = hash & (tab.length - 1);
}
//真正增加元素的地方,传入了hash值和下标
addNewEntry(key, value, hash, index);
return null;
//存入键值为null的值
private V putValueForNullKey(V value) {
//entryForNullKey对象专门持有该空键的值
HashMapEntry<K, V> entry = entryForNullKey;
if (entry == null) {
//新增
addNewEntryForNullKey(value);
size++;
modCount++;
return null;
} else {
//替换并返回旧值
preModify(entry);
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
//注意新建entry时next域传入头结点,添加新entry时,会采用头插法
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
//新建key为null的entry,传入的键值为null,hash为0
void addNewEntryForNullKey(V value) {
entryForNullKey = new HashMapEntry<K, V>(null, value, 0, null);
}
为什么初始容量必须是2的幂且扩容必须是2倍?为什么下标index算法为int index = hash & (tab.length - 1);?
初学数据结构与算法,hash表的下标使用取模计算的,这里为什么用位运算?先上结论吧,这两个问题其实是相关联的,当tab.length 为2的幂的时候,hash&(tab.length - 1)等价于hash%tab.length。
取模的方法,本质上就是hash不停除以长度,直到结果小于除数。而按位与,在二进制下分两部分看:(1)hash值中比length高的字节,一与之后全没了,因为length的高字节全是0,那写被与的高字节其实就是length的倍数,准确的说是2的幂倍,相当于除了好多length。(2)除完之后的hash值二进制为全0或全1,即00000。。~11111。。即实际范围(0到length+length-1)而length二进制为1000。。。,hash值还是比长度大,length-1的二进制都是形如011111。。。,此时的hash与length-1一与,比length大的数就会减去一个length,而比length小的数不会变,之后范围就会变成(0~length-1),这样就对应了数组的下标了。
hash算法
由键的hashCode和Collections的hash算法决定
int hash = Collections.secondaryHash(key);
//Collections里面的静态方法
public static int secondaryHash(Object key) {
//调用key对象的hashCode方法
return secondaryHash(key.hashCode());
}
//真正产生hash值的地方,真正的hash值两个因素决定:1、键的hashCode值。2、此方法里面的算法
private static int secondaryHash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
扩容
若果元素个数大于扩容阈值,就2倍扩容
private HashMapEntry<K, V>[] doubleCapacity() {
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
//两倍扩容
int newCapacity = oldCapacity * 2;
//使用新的容量建立新的entry数组,数组是空的,并且对扩容阈值重新计算赋值
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;
}
//highBit为0代表index小于length,为length代表index大于length,详见上面红字的问题
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
//重新散列
newTable[j | highBit] = e;
//遍历hash冲突的链表,从第二个元素开始
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
//对链表内的元素也重新hash
int nextHighBit = n.hash & oldCapacity;
//不同的话就说明相邻两个节点要断开,n节点要划分
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;
}
在对链表进行再hash时,需要有三个指针,broken指向要断开的地方,e指向前一个节点、n指向当前要判断的节点