简介
- 使用哈希表的方式实现了所有Map接口的功能。该类允许null作为key与value值。HashMap与HashTable相比,除了HashMap不支持同步操作,以及其允许null作为key/value值,基本实现一致。该类不保证Map存储的顺序保持不变(需扩容)
- 在Hash方法将元素合理的分布在每个槽内时,该类提供了基本操作的常量级时间复度(get/put方法)。
- 集合视图上的迭代所需的时间与HashMap实例的“容量”(存储桶数)及其大小(键-值映射数)成正比。 因此,如果迭代性能很重要,则不要将初始容量设置得过高(或负载因子过低),这一点非常重要。
- HashMap的实例具有两个影响其性能的参数:初始容量和负载因子。 容量是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。 负载因子是在自动增加其哈希表容量之前允许哈希表获得的满度的度量。 当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即,内部数据结构将被重建),因此哈希表的存储桶数大约为两倍。
- 通常,默认负载因子(.75)在时间和空间成本之间提供了一个很好的折衷方案。
较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到体现,包括get和put)。
设置映射表的初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少重新哈希操作的数量。 如果初始容量大于最大条目数除以负载因子,则将不会进行任何重哈希操作。 - 如果可以预知大量元素将会被存在hashMap中,预制一个足够容量大小的HashMap相比后续一直进行重哈希扩容操作会更有效率。请注意,设置拥有同样哈希值的key值,会导致hashMap性能变差 。为了改善影响,当key的类实现了Comparable接口时,该类会使用比较顺序来提升性能表现。(how?)
- 该类不支持同步操作,多线程操作hashmap,且存在线程对hashMap进行结构修改(结构修改是添加或删除一个或多个键值对的任何操作;仅更改与实例已经包含的键相关联的值不是结构修改)时,必须在操作外部进行同步操作。 这种同步操作通常采用在Map类中某个对象进行同步的方式进行,如果Map类内部不存在这种对象,可以为map类包装一个同步Map类的实现。(Hashtable)
具体实现;
- 初始化:初始容量16,负载因子0.75,map大小是2的幂次方
- 哈希方法:对对象取哈希值,并对其高16位以及低16位进行异或操作。为什么呢?为了使高位参与运算,使哈希值更散列。
- put方法,具体实现在putval方法中,实现过程如下
/**
* 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<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);
//如果该位置已经存在元素,P目前指向该元素
else {
Node<K,V> e; K k;
//如果当前位置存储元素的key值,与此次put传入的key值相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //e指向当前存在的元素
//p是一个TreeNode(代表当前节点已存在不止一个hash冲突),做树插入(可能是红黑树,平衡二叉树)
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;
}
}
//map中存在同样key的元素,返回修改后的旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//修改put次数统计值,并判断是否需要rehash
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
主要 流程
按照key值取hash值,再对hash值取模,找到该元素存储的位置。进行插入。
get方法,主要在getNode方法中实现,主要逻辑其实个put方法是对应的,分为单个节点获取value,链表获取value,以及树结构获取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;
//
if ((e = first.next) != null) {
//如果是树结构,在树中查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)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;
}
再哈希方法
与hashmap无法多线程场景下使用有关,具体实现如下:
/**
* 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<K,V>[] resize() {
Node<K,V>[] oldTab = table;//当前的哈希表
int oldCap = (oldTab == null) ? 0 : oldTab.length;//当前哈希表大小
int oldThr = threshold;//当前预计扩容大小
int newCap, newThr = 0;
//当前哈希表大小大于0 ,扩充新表大小为原表2倍
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//极端情况,设置为Integer.MAX_VALUE
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);
}
//首次生成新的threshold
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//遍历旧哈希表,将其中元素重新插入新哈希表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
//槽中是列表结构
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
1.7以及1.8的HashMap实现不同:
-
1.7是数组+链表,1.8则是数组+链表+红黑树结构;
-
jdk1.7中当首次put元素,哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;
-
插入键值对 的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
-
jdk1.7中的hash函数对哈希值的计算直接使用key的hash值,1.8中则是哈希值对其高16位以及低16位进行异或操作,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
-
扩容:
1.8会保持原链表的顺序,而1.7会颠倒链表的顺序(头插发);
1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前; -
jdk1.8是扩容时,重定位元素位置,hash值不变,所以元素位置为当前位置,或当前位置+原哈希表大小,而1.7是通过更新hashSeed来修改hash值达到分散的目的,需要全部重新定位元素;
-
扩容策略:
1.7直接扩容2倍
1.8