1 . HashMap 概述:
Hash 表实现了Map接口,可以执行Map所含的所有方法,并且允许Null key和Null值;(HashMap和Hashtable是大部分是相同的,除了前者是非同步的且允许Null对象!)HashMap是无序的,它并不保证元素的顺序随着时间变化而不变。
HashMap对外提供表现一致的put和get方法,提供一个hash函数对容器里面的元素进行分散到固定块存储,并且随着时间的推进,内部块的大小会成比例的增加。因此不需要在初始化时把容器大小设置的过大;
每个HashMap的实例包含两个参数会影响到它的表现,分别是initial capacity和load factor;这个capacity是Hash表内部的容器空间大小,而load factor是当前容器允许的最大可承载元素大小的指数;当容器内部元素个数接近达到load factor要求及current capacity大小时,这个Hash 表就会重新构建分布(内部数据结构会重构),它的容器大概会扩增到之前大小的两倍左右;
通常默认的load factor是 (.75),这时会使时间与空间消耗的效率达到最优。比这个值高可以节约一点空间消耗,但会增加Map相关所有操作的查找时间(包括put,get,等)。当设置Hash表的初始大小时,应该把所预计的实体大小和load factor的大小一起计算到其中,以减少rehash的次数;如果Hash表的初始大小大于由load factor分散的预计实体集合的大小,那就永远不会发生rehash操作;
当我们要用一个Hash表存放大量的映射对象时,可以创建一个初始化大小很大的对象,那样会提高put的效率;请注意,如果操作很多对于key的hashCode一样的映射对象时,无疑对于任何hash table都会降低效率,因此为了降低这种影响,当key对象实现了Comparable时,可以用对应class的比较方式对这种key的顺序进行区分;
(内部首先是一个根据hash key定位的数组,然后数组中的每一个元素都应该是一个链表)
Map m = Collections.synchronizedMap(new HashMap(...));
对于从Hash表得到的iterators是立即失败的,如果我们在获取到一个集合对象的迭代器后,对集合的结构进行修改,然后再执行iterators的相关方法,那iterator就会抛出一个异常ConcurrentModificationException。但是通常来讲,对于任何不并发的数据结构,我们都不能保证这种快速失败是立即生效,所以依据此规则编写我们的应用是错误的,迭代器可以快速的给我们传递集合已被修改的信息。这一点仅可以被我们用来查找bug。
2 . 相关内容 (下面的全部依据jdk1.8,因为java8对HashMap进行了优化)
1) put 函数的实现思路:
2) put函数具体实现代码如下:
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//节点为空,直接赋值
else {//
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//通过树内部逻辑处理
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3) get 函数的实现思路
4) get函数具体实现代码如下:
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
5) hash函数实现:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//这里采用了,高位不变,低16位与高16位做了一个异或(这是一个顾全大局的做法,综合考虑了速度,作用,质量)。
}
jdk1.8之前的Hash函数实现:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);//如果是字符串的key,就使用系统的stringHash32函数计算对应的hash值
}
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);
}
6) resize函数实现:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3 . 其他
关于Java8的优化, 并不是当出现相同Hash Key时的多个元素就立即使用的树结构存放同Hashkey的节点, 大概的逻辑是, 最开始还是用的链表数据结构(HashMap.Node), 而当链表大小超过7时, 才替换为红黑树(HashMap.TreeNode);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
注意HashMap是非同步的,所以当有多个线程操作连表时并且有一个线程在修改它的结构时,我们要自己保证同步线程安全,可以通过它实现:
注意HashMap是非线程安全的, 当在多线程环境下同时访问它时会出几个问题,
<1>. 添加节点的操作与map size加一操作不是原子操作, 在多线程情况下同时进行元素添加会丢失数据,最终导致会出现size的表现值与Map实际持有元素个数不一致的情况发生;
<2>. 当Map由于元素增多而需要扩容时, 生成比原始容器大的新容器时会进行元素转移(transfer)的过程中, 多线程访问可能会造成链表元素闭环, 这样等下一次get操作命中该链表时就可能会产生死循环;
在多线程并发环境下, 应该使用位于java.util.concurrent包下的ConcurrentHashMap集合对象;