是很常见的类,前段时间公司系统由于对HashMap使用不当,导致cpu百分之百,在并发环境下使用HashMap 而没有做同步,可能会引起死循环,关于这一点,sun的官方网站上已有阐述,这并非是bug。
HashMap的数据结构
transient Entry[] table;
Entry就是HashMap存储数据所用的类,它拥有的属性如下
final K key;
V value;
final int hash;
Entry<K,V> next;
看 到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好 吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的 Entry的next属性。数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。
几个关键的属性
存储数据的数组
transient Entry[] table;
默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
默认加载因子,加载因子是一个比例,当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当实际数据大小超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
int threshold;
加载因子
final float loadFactor;
构造函数1
- public
HashMap(int initialCapacity, float loadFactor) { -
if (initialCapacity < 0) -
throw new IllegalArgumentException ("Illegal initial capacity: " + -
initialCapacity); -
if (initialCapacity > MAXIMUM_CAPACITY) -
initialCapacity = MAXIMUM_CAPACITY; -
if (loadFactor <= 0 || (loadFactor)) -
throw new IllegalArgumentException ("Illegal load factor: " + -
loadFactor); -
-
// Find a power of 2 >= initialCapacity -
int capacity = 1; -
while (capacity < initialCapacity) -
capacity <<= 1; -
-
this.loadFactor = loadFactor; -
threshold = (int)(capacity * loadFactor); -
table = new Entry[capacity]; -
init(); - }
- while
(capacity < initialCapacity) -
capacity <<= 1;
capacity才是初始容量,而不是initialCapacity,这个要特别注意,如果执行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9,想想为什么吧。
构造函数2
- public
HashMap(int initialCapacity) { -
this(initialCapacity, DEFAULT_LOAD_FACTOR); -
}
- public
HashMap() { -
this.loadFactor = DEFAULT_LOAD_FACTOR; -
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); -
table = new Entry[DEFAULT_INITIAL_CAPACITY]; -
init(); -
}
- public
HashMap(Map<? extends K, ? extends V> m) { -
this(((int) (() / DEFAULT_LOAD_FACTOR) + 1, -
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); -
putAllForCreate(m); -
}
int hash = hash(());
hash函数如下:
- static
int hash(int h) { -
return useNewHash ? newHash(h) : oldHash(h); -
}
- private
static final boolean useNewHash; -
static { useNewHash = false; }
- private
static int oldHash(int h) { -
h += ~(h << 9); -
h ^= (h >>> 14); -
h += (h << 4); -
h ^= (h >>> 10); -
return h; - }
-
- private
static int newHash(int h) { -
// 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); - }
如果确定数据的位置
看下面两行
- int
hash = hash(()); - int
i = indexFor(hash, );
第一行,上面讲过了,是得到哈希值,第二行,则是根据哈希指计算元素在数组中的位置了,位置的计算是将哈希值和数组长度按位与运算。
- static
int indexFor(int h, int length) { -
return h & (length-1); -
}
“h & (length-1)”
首先我们要确定一下,HashMap的数组长度永远都是偶数,即使你在初始化的时候是这样的new HashMap(15,0.75);因为在构造函数内部,上面也讲过,有这样的一段代码:
- while
(capacity < initialCapacity) -
capacity <<= 1;
所以length-1一定是个奇数,假设现在长度为16,减去1后就是15,对应的二进制是:1111。
假设有两个元素,一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111与运算后,分别还是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。
那么,如果数组长度是奇数,减去1后就是偶数了,偶数对应的二进制最低位一定是 0了,例如14二进制1110。对上面两个数子分别与运算,得到1000和1000。看到了吗?都是一样的值,哈希值8和9的元素多被存储在数组同一个位 置的链表中。在操作的时候,链表中的元素越多,效率越低,因为要不停的对链表循环比较。所以,一定要哈希均匀分布,尽量减少哈希冲突,减少了哈希冲突,就 减少了链表循环,就提高了效率。
put方法到底作了什么?
- public
V put(K key, V value) { -
if (key == null) -
return putForNullKey(value); -
int hash = hash(()); -
int i = indexFor(hash, ); -
for (Entry<K,V> e = table[i]; e != null; e = ) { -
Object k; -
if ( == hash && ((k = ) == key || (k))) { -
V oldValue = ; -
= value; -
(this); -
return oldValue; -
} -
} -
-
modCount++; -
addEntry(hash, key, value, i); -
return null; -
}
- private
V putForNullKey(V value) { -
int hash = hash(NULL_KEY.hashCode()); -
int i = indexFor(hash, ); -
-
for (Entry<K,V> e = table[i]; e != null; e = ) { -
if ( == NULL_KEY) { -
V oldValue = ; -
= value; -
(this); -
return oldValue; -
} -
} -
-
modCount++; -
addEntry(hash, (K) NULL_KEY, value, i); -
return null; -
}
这一段代码是处理哈希冲突的,就是说,在数组某个位置的对象可能并不是唯一的,它是一个链表结构,根据哈希值找到链表后,还要对链表遍历,找出key相等的对象,替换它,并且返回旧的值。
- for
(Entry<K,V> e = table[i]; e != null; e = ) { -
if ( == NULL_KEY) { -
V oldValue = ; -
= value; -
(this); -
return oldValue; -
} -
}
- modCount++;
- addEntry(hash,
(K) NULL_KEY, value, i); - return
null;
- void
addEntry(int hash, K key, V value, int bucketIndex) { - Entry<K,V>
e = table[bucketIndex]; -
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); -
if (size++ >= threshold) -
resize(2 * ); - }
- Entry(int
h, K k, V v, Entry<K,V> n) { -
value = v; -
next = n; -
key = k; -
hash = h; -
}
上面的put方法里有这样的一段:
- if
(size++ >= threshold) -
resize(2 * );
- void
resize(int newCapacity) { -
Entry[] oldTable = table; -
int oldCapacity = ; -
if (oldCapacity == MAXIMUM_CAPACITY) { -
threshold = Integer.MAX_VALUE; -
return; -
} -
-
Entry[] newTable = new Entry[newCapacity]; -
transfer(newTable); -
table = newTable; -
threshold = (int)(newCapacity * loadFactor); -
}
- void
transfer(Entry[] newTable) { -
Entry[] src = table; -
int newCapacity = ; -
for (int j = 0; j < ; j++) { -
Entry<K,V> e = src[j]; -
if (e != null) { -
src[j] = null; -
do { -
Entry<K,V> next = ; -
int i = indexFor(, newCapacity); -
= newTable[i]; -
newTable[i] = e; -
e = next; -
} while (e != null); -
} -
} -
}
如何寻找元素
根据key的hasocde()得到哈希值
根据哈希值确定
找到指定位置 的链表,循环比较,先“==”比较,如果不等,再“equals”比较,如果有一个比较相等,就说明找到元素了。
所以说到这里,我想大家也明白了,为什么要把一个对象放进HashMap的时候,最好是重写hashcode()方法和equals 方法呢?根据前面的分析,hashcode()可以确定元素在数组中的位置,而equals方法在链表的比较时要用到。
正确的使用HashMap
1:不要在并发场景中使用HashMap
看看get方法的红色部分
- public
V get(Object key) { -
if (key == null) -
return getForNullKey(); -
int hash = hash(()); -
for (Entry<K,V> e = table[indexFor(hash, )]; -
e != null; -
e = ) { -
Object k; -
if ( == hash && ((k = ) == key || (k))) -
return ; -
} -
return null; -
}