HashMap和Hashtable作为保存键值对的容器,都是使用一个Entry数组,Entry元素本身又是一个链式结构,所以实现数据结构相同,都是一个数组-链表形式。(Entry是Map接口的一个内部接口,HashMap实现了Map接口,HashMap的内部类Entry实现了Map接口的内部接口Entry)
HashMap
主要的属性:
<span style="font-family:KaiTi_GB2312;">static final int DEFAULT_INITIAL_CAPACITY = 16;//默认容量,2的整数次幂
static final int MAXIMUM_CAPACITY = 1 << 30;//默认最大容量,当构造map时给出了超出此大小,则使用该值
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子
transient Entry<K,V>[] table;//底层entry数组,声明为transient原理与ArrayList相同
transient int size;//entry键值对的个数
int threshold;//键值对的阈值,达到则扩容为2倍capacity
final float loadFactor;//负载因子
transient int modCount;//修改次数,迭代中用于检测发出快速失败</span></span>
HashMap实现有:
<span style="font-family:KaiTi_GB2312;">public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable</span>
此处实现了Serializable接口,表明对象可以进行序列化,Entry数组以transient修饰,表示对象序列化操作忽略关于Entry数组部分,HashMap类中存在writeObject和readObject方法,在执行序列化时由HashMap 对象本身处理Entry数组的序列化和反序列化。同ArrayList,参见ArrayList的remove、序列化
Map的Map.Entry结构:
<span style="font-family:KaiTi_GB2312;">static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {//构造新元素、节点的参数为四个
value = v;
next = n;
key = k;
hash = h;
}
.......
}</span>
Map中包含一个Entry<K,V>[]类型的 table 数组,每个元素为Entry类型,包含一个链表结构用于解决hash冲突
以Map的两个主要函数,put、get说明冲突:
<span style="font-family:KaiTi_GB2312;">public V put(K key, V value) {
if (key == null)
return putForNullKey(value);//如果key为null,执行专有的putForNullKey
int hash = hash(key);//根据构造对象时的hashcode,重新计算hash值
int i = indexFor(hash, table.length);//根据重新计算的hash值,找出对应的table的下标位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;//在table[i]的链表中,如果存在hash值相等并且key值相等的Entry,则覆盖其value值
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);//如果在table[i]中不存在相等的Entry(key相等)
return null; //则在table[i]以头插的方式,添加Entry
}</span>
如果执行put操作的key为null,则在put中执行putForNullKey:
<span style="font-family:KaiTi_GB2312;">private V putForNullKey(V value) {//如果key为null,则value值放在table数组下标为0的位置,从该函数可以看出
for (Entry<K,V> e = table[0]; e != null; e = e.next) {//键值对的value可以作为key的附属
if (e.key == null) { //put操作时不在乎value为何值
V oldValue = e.value; //如果table[0]已经存在key为null的Entry,则覆盖value,否则addEntry
e.value = value; //跟正常插入一样执行addEntry,只不过此时i的值为0,key为null
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//如果table[0]的链表中没有key为null的节点,则插入key为null的节点
return null;
}</span>
如果当前key节点对应的Entry不存在,则无论key是否为null,都正常执行addEntry:
<span style="font-family:KaiTi_GB2312;">void addEntry(int hash, K key, V value, int bucketIndex) {//重新计算后的hash值,key,value,table下标
if ((size >= threshold) && (null != table[bucketIndex])) {//已经存在的Entry个数size达到阈值并且table
resize(2 * table.length); //下标的此处的Entry不为null,说明了扩展table需要两个条件都满足
hash = (null != key) ? hash(key) : 0;//扩展后重新计算该key的哈希值
bucketIndex = indexFor(hash, table.length);//以及在新的table中的下标bucketIndex
}
createEntry(hash, key, value, bucketIndex);//无论是否扩展,hash、bucketIndex是否变化,照常添加新Entry
}
void createEntry(int hash, K key, V value, int bucketIndex) {//添加新Entry,头插方式
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);//原来的头结点即table[bucketIndex],作为next
size++;
}</span>
关于resize,table数组长度变为两倍,即table数组的长度一直为2的整数次幂,参考indexFor:
<span style="font-family:KaiTi_GB2312;">static int indexFor(int h, int length) {
return h & (length-1);
}</span>
重新计算后的hash值h,与table数组长度减一值进行与运算,返回数组下标bucketIndex。
length作为2的整数次幂,length-1表示有效位都为1,进行与运算保证结果bucketIndex,始终处于length范围内。
计算数组下标Hashtable选择的是求余运算(为了对比,提前说一下Hashtable):
<span style="font-family:KaiTi_GB2312;">index = (hash & 0x7FFFFFFF) % tab.length;</span>
将重新计算后的hash值与0x7FFFFFFF相与,得出一个正数值,再对数组长度求余。选择与0x7FFFFFFF相与而不是使用Math.abs方式取正整数,一是因为计算效率高,二是Math.abs在值为Integer.MIN_VALUE时,得到的仍然是负数,对tab的数组长度求余,尽量选择tab.length为质数,Hashtable选择的数组长度始终为奇数。
<span style="font-family:KaiTi_GB2312;">int newCapacity = (oldCapacity << 1) + 1;</span>
在HashMap中,一直说hash值为重新计算后的哈希值,根据key的hashCode重新计算出hash:
<span style="font-family:KaiTi_GB2312;">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);//返回重新计算后的哈希值
}</span>
如果key为字符串,则直接利用字符串中定义的哈希值即可,String类或Integer等属于常量类,其中定义的哈希值在内容不相同的情况下不会重复,可以直接利用。
额外提一下:
在并发环境中容易发生死锁问题,避免死锁的四个条件的一个简单方式就是按顺序加锁,如果不能获得全部锁,则释放已有的锁,实现按顺序加锁的一个策略可以是将锁按照其哈希值排列,按照哈希值的大小进行获取,这也是为什么在转账过程中最好使用账号标志,作为一个字符串,只要账号不重复,则所有的操作可以按相同的顺序获取两个账号,实现按顺序加锁,以避免死锁。
从hash函数中可以看出,重新计算hash值是在获取对象本身的hashCode后进行了几次无符号右移,实现高位低位都参与到后续的对table数组长度减一的与运算中,避免出现高位不同,低位相同,而导致的散列冲突。
在resize函数中存在一个消耗性能的地方,就是transfer:
<span style="font-family:KaiTi_GB2312;">void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//遍历旧table数组
while(null != e) {//遍历每个Entry形成的链表
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;
}
}
}</span>
重新计算i也就是之前的bucketIndex,然后头插方式赋予引用关系。
至于get、containsKey方法则是调用getEntry:
<span style="font-family:KaiTi_GB2312;">final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);//计算hash值
for (Entry<K,V> e = table[indexFor(hash, table.length)];//遍历table[bucketIndex]
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}</span>
这里多说一句,根据get返回值并不能知道Map中是否包含对应的key,如果key不存在,则返回null,如果key存在但是对应value为null,返回仍为null,所以可以利用返回boolean类型的containsKey来判断。
总结:
HashMap作为键值对的集合,底层结构为一个数组加链表的形式实现,以拉链发解决散列冲突,table数组的长度为2的正整数次幂,实现中重新计算了key对象的hash值并利用与运算来定位bucketIndex,value可以作为key的附属,都可以为null,key为null时键值对形成的Entry处于table[0]处,put操作时如果key值计算出的bucketIndex相同,则遍历table[bucketIndex]的Entry链表,如果存在key值相同(e.key==key||key.equals(e.key)),则覆盖value,否则头插法插入新Entry节点。