更多 Java 集合类方面的文章,请参见文集《Java 集合类》
Java 集合
- Java 集合实际上是多个 引用变量 组成的集合,这些引用变量指向实际的对象
- 并不会真正地将对象放入集合中
Map.Entry
- 为 Map 中的元素,表示为 key - value pair
interface Entry<K,V>
Bucket
A bucket is used to store key - value pairs.
In hash map, bucket use simple linked list to store object. (与 java.util.LinkedList 不同,it is a separate and simple implementation just for map)
对象中的两个方法:
-
equals() - 返回 boolean
- 自反
- 对称
- 传递
-
hashCode() - 返回 int
- 若两个对象通过 equals() 判断相等,则 hash code 肯定相等
- 反之不一定
HashMap 的实现: 链表散列:数组 + 链表
Node<K,V> HashMap 中元素的存储结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
影响 HashMap 性能的两个因素:
-
initial capacity 即初始 bucket 的数目
-
DEFAULT_INITIAL_CAPACITY
为 16
-
-
load factor - is the measure of how full the hash table is allowed before its capacity is automatically increased.
-
DEFAULT_LOAD_FACTOR
为 0.75 - 若超过 load factor,则需要重新 rehash,整个 Map 需要重建
- load factor 越大:空间利用率高,冲突概率大,链表长,查找效率低
- load factor 越小:空间利用率低,冲突概率小,链表短,查找效率高
-
Java8 HashMap 插入元素的过程
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)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> 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<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();
afterNodeInsertion(evict);
return null;
}
从中可以看出:
- 通过 key 的 hash code 计算其在数组中的索引:
if ((p = tab[i = (n - 1) & hash]) == null)
-
(n - 1) & hash
:确保索引在数组范围内 - 为什么不直接用 hash 对 数组长度取模?因为除法运算效率低
-
- 如果数组中对应的索引位置上已经有元素了:
- 如果该元素的 key 与要添加的 key 相同(通过 equals 方法比较),则不添加,返回已存在元素的 value
- 否则,将该位置设为新的 Node,原先的 Node 后移,即设置为新 Node 的 next,返回
-
++modCount;
标明该 HashMap 已被修改
为什么哈希表的容量 n 一定是2的幂
比如2,4,8,16,32
- 若 n 为 2 的幂,则
(n - 1) & hash
相当于对 n 取模,确保了散列的均匀,同时提升了效率 - 若 n 为偶数,
(n - 1)
为奇数,最后一位为 1,&与运算后,最有一位可能是 0,也可能是 1 - 若 n 为奇数,
(n - 1)
为偶数,最后一位为 0,&与运算后,最有一位只可能是 0,浪费了一半的空间
HashMap VS HashTable
- 继承方式不同:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
关于 Dictionary 这个接口,API 中有如下描述:
NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class.
-
是否线程安全:
-
HashMap 非同步,线程不安全。可以通过
Collections.synchronizedMap(m);
来创建线程安全的 HashMap。 - Hashtable 同步,线程安全。Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。
-
HashMap 非同步,线程不安全。可以通过
-
key 和 value 是否可以为 null:
- HashMap 中 key 和 value 都可以为 null。因此 get(key) 为 null 并不能代表该 key 不存在,需要使用 containsKey(key) 来判断。
- Hashtable 中 key 和 value 都不可以为 null。
-
数组索引的计算方式:
- HashMap:在 key 的 hash code 的基础上通过
(n - 1) & hash
来计算数组的索引 - Hashtable:直接使用 key 的 hash code 与数组长度求余作为数组的索引
int index = (hash & 0x7FFFFFFF) % tab.length;
- HashMap:在 key 的 hash code 的基础上通过
-
数组的初始容量及扩容方式
- HashMap:初始容量为16,扩容
2 * old size
- Hashtable:初始容量为11,扩容
2 * old size + 1
- HashMap:初始容量为16,扩容