本文参考:http://blog.youkuaiyun.com/happy_horse/article/details/52316865
关于HashMap,你应该知道的几个知识点:
HashMap多线程的条件竞争;
不可变对象的好处;
为什么String, Interger这样的wrapper类适合作为键;
1、HashMap内部结构组成及实现
HashMap是基于哈希表(使用了拉链法解决Hash冲突的哈希表)的一个Map接口实现,类中具体的实现是基于数组和链表;存储的对象是一个键值对对象(Entry
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
链表节点Entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
·····
//其重写了equals()、hashcode()和toString()方法
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();
}
}
2、HashMap类
构造函数:
public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()
public HashMap(Map<? extends K, ? extends V> m)
第二和三个构造函数都是调用了第一个函数。第四个函数不讲
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小于0,hashmap类默认初始容量为16
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//初始容量不能超过最大值MAXIMUM_CAPACITY,其默认值为2的30次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
这是HashMap的构造函数之一,其他构造函数都引用这个构造函数进行初始化。
参数InitialCapacity指的是HashMap中table数组最初的大小,
参数loadFactory指的是HashMap可容纳键值对与数组长度的比值(举个例子:数组长度默认值为16,loadFactory默认值为0.75,如果HashMap中存储的键值对即Entry多于12,则会进行扩容)。
参数threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子),若table是空表时,其等于初始容量。
注意:
在构造函数中不会对数组进行初始化,只有在put等操作方法内会进行判断是否要初始化或扩容。
3、put()方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
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方法时,首先判断table是否是空数组,若是空数组,则调用inflateTable(int)方法对数组进行初始化,其输入参数为初始设置的InitialCapacity。
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];
initHashSeedAsNeeded(capacity);
}
在构建table数组时,数组的容量必须是2的幂数(原因稍后解释),所以put()方法首先调用roundUpToPowerOf2(int)方法,判断初始值是否是2的幂数;若是,则使用这个初始值;若不是,则将其改为一个比初始值大的最小的2的幂数,用来保证table数组的容量永远是2的幂数。
在构建完数组后,将会判断存入元素的key值是否为空(这里说明了HashMap是允许存入对象的键值为null的),所有键为空的对象都会存入table[0]中。
若键值不为空,则调用hash()方法,结合对象的key的哈希值获取整个对象的哈希值,以最大程度的确保相同对象会有相同的哈希值,不同对象会有不相同的哈希值:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String)
{
return
sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
在获取对象的哈希值后,会调用indexFor(int h, int length)方法,来确定对象放在table数组的什么地方。
static int indexFor(int h, int length) {
return h & (length-1);
}
我们知道对于HashMap的table而言,数据的分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素均匀分布呢?我们会想到取模,indexFor()方法相当于h对length取模。但如何保证元素的均匀分布呢?这就是系统要求table数组的容量必须是2的幂数的原因。如图:
从上面的图表中我们看到当length=15时,总共发生了8此碰撞(最后一列为数组中的位置),同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。
而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
在计算出对象存放在table数组中的位置后,将取出对应位置上的链表头元素,并循环单链表:
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;
}
}
其中,hashMap通过对象的hash值,来获取其存放位置,如果存放位置有多个对象,则通过key.equals()方法来判断,是否是一个对象,是则覆盖其value值。这说明,HashMap通过hash()和equals()来保证对象的唯一性。
如果链表中不存在当前对象,则将当前对象插入到链表的头部。调用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);
}
在插入前,首先要进行容量判断,若当前table[]中的对象个数>=先前设定好的阀值时(threshold = 数组容量 * 负载因子),同时对象将要插入的位置已经有别的对象了时,将会对当前table[]进行自动扩容。新的容量为原来容量的2倍,扩容方法是resize():
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));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
如果原来的table[]的容量已经是最大值MAXIMUM_CAPACITY,则不进行扩容,而是将阀值扩大到最大值MAXIMUM_CAPACITY;若table[]的容量不是最大值,则进行扩容;扩容后,需要重新对table[]中已经存储的对象进行再hash,使用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;
}
}
}
注意对每个已存储对象再hash后,其存储链表顺序与扩容前的链表顺序相反。
容量扩充完毕后,再计算其在table[]中的存储位置,将其插入到存储位置链表的头部。
4、get()方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
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;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
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是否为空进行判断,因为key为空的对象都存储在table[0]中;若不为空,则按序对table[]和其存的链表进行顺序遍历。和put()方法步奏相同,先求出对象的hash值,然后计算其在数组中的位置,然后顺序遍历其对应链表,使用key.equals()方法或==进行key比较,找到所需的对象。
其他函数:
克隆函数:是浅拷贝
不可变对象的好处:
String, Interger这样的wrapper类作为HashMap的键是很合适的,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。