从本文你可以学到什么
HashMap 实现了Map接口;使用键值对的数据结构组织数据。
在官方文档中这样描述HashMap:
在这段描述中说明了HashMap的特点:
使用示例:
//创建一个Map集合
HashMap<String,String> map=new HashMap();
//向Map中插入元素
map.put("a-key","a-value");
map.put("b-key","b-value");
//获取Map中的元素
System.out.printf(map.get("a-key"));
System.out.printf(map.get("b-key"));
//遍历Map中的元素
for(Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
运行后的结果:
a-value
b-value
a-key: a-value
b-key: b-value
HashMap创建实例时有一个如下构造方法
HashMap(int initialCapacity, float loadFactor)
第一个参数中指定了初始容量(initialCapacity),第二个参数指定了负载系数(loadFactor);
所以在HashMap中的两具重要参数分别外容量(Capacity)和负载系数(Load factor);
容量即指整个HashMap桶的大小;负载系数的作用用于扩容,当HashMap中的元素个数大于Capacity*loadFactor时就会触发扩容机制;默认每次扩容为当前容量大小的两倍;系统默认负载系数为0.75。
方法原型:
public <V> get(Object key);
调用方法将返回HashMap集合中指定Key所映射的值,如果Key不存在于集合中将返回null。
实现思路:
源代码:
public V get(Object key) {
Node<K,V> e;
//计算key的hash值并调用getNode方法
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> 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;
//存在hash的Node但未直接命中Key
if ((e = first.next) != null) {
//遍历树查找key
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//遍历链表查找key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
方法原型:
public V put(K key, V value)
将指定值与该映射中的指定键相关联。如果该映射先前包含该键的映射,则将替换旧值。
实现思路:
源代码:
public V put(K key, V value) {
//计算Key的hash值并调用putVal
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//没有Node直接创建将将值给这个Node
tab[i] = newNode(hash, key, value, null);
else {
//已经Key在槽中的了
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//对于不同的类型进行putVal操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)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操作
resize();
afterNodeInsertion(evict);
return null;
}
在get与put操作中都会对key计算hash值,这个计算过程由hash函数实现;那这个函数的实现思路是什么样的呢?
hash函数思路:
源代码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
最后在get与put函数中会使用(length-1)&hash的方式来计算key在当前集合中的索引位置。
设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在n - 1为15(0x1111)时,其实散列真正生效的只是低4bit的有效位,当然容易碰撞了。
因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
如果还是产生了频繁的碰撞,会发生什么问题呢?作者注释说,他们使用树来处理频繁的碰撞(we use trees to handle large sets of collisions in bins),在JEP-180中,描述了这个问题:
之前已经提过,在获取HashMap的元素时,基本分两步:
在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
因此在Java 8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题。