JDK1.7中的HashMap是由数组+链表组成的,而JDK1.8中的HashMap是由数组+链表+红黑树组成。数组的默认长度(DEFAULT_INITIAL_CAPACITY
)为16(能够自动变长,也能够指定一个长度),加载因子(DEFAULT_LOAD_FACTOR
)为0.75。node
数组:具备遍历快,增删慢的特色。数组在堆中是一块连续的存储空间,遍历时数组的首地址是知道的,因此遍历快,遍历的时间复杂度为
O(1)
;当在中间插入或删除元素时,会形成该元素后面全部元素地址的改变,因此增删慢,增删的时间复杂度为O(n)
。数组
链表:链表具备增删快,遍历慢的特色。链表中各元素的内存空间是不连续的,一个节点至少包含节点数据与后继节点的引用,因此在插入删除时,只需修改该位置的前驱节点与后继节点便可,链表在插入删除时的时间复杂度为
O(1)
。可是在遍历时,get(n)元素时,须要从第一个开始,依次拿到后面元素的地址,进行遍历,直到遍历到第n个元素,遍历的时间复杂度为O(n)
,因此效率极低。安全
指两个元素经过hash函数计算出的值是同样的,表示这两个元素存储的是同一个地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了哈希冲突。bash
哈希冲突的解决办法:多线程
HashMap
使用的是链地址法。在JDK1.7中,若是链表过长,效率就会大大下降,查找和添加操做的时间复杂度都为O(n)
;在JDK1.8中,若是链表长度大于8,链表就会转化为红黑树,时间复杂度也就将为O(logn)
,性能获得了很大的提高。app
当红黑树节点个数少于6的时候,又会将红黑树转化为链表。由于在数据量较小的状况下,红黑树要维持平衡,比起链表,性能上的优点并不明显。函数
若是HashMap的大小超过了负载因子(默认为0.75)定义的容量,也就是说,当一个Map填满了75%的Bucket时候,将会建立原来HashMap大小的两倍的Bucket数组,来从新调整Map的大小,并将原来的对象放入新的Bucket数组中。源码分析
阈值 = 数组默认的长度 x 负载因子(阈值 = 16 x 0.75 = 12)性能
1. HashMap扩容限制的负载因子为何是0.75?为何不能是0.1或者1呢?ui
2. 为什么数组容量必须是2次幂? 索引计算公式为index = (length - 1) & hash
,若是length为2次幂,那么length-1的低位就全是1,哈希值进行与操做时能够保证低位的值不变,效果等同于hash%length
,从而保证分布均匀。
JDK1.8中在扩容HashMap的时候,不须要像JDK1.7中去从新计算元素的hash,只须要看看原来的hash值新增的哪一个二进制数是1仍是0就行了「是0仍是1能够认为是随机的」,若是是0的话表示索引没有变,是1的话表示索引变成“oldCap+原索引”,这样即省去了从新计算hash值的时间,而且扩容后链表元素位置不会倒置。
![]()
JDK1.7中的HashMap是由数组+链表组成的,组成链表结点的是Entity
包含三个元素:key
、value
和指向下一个Entity的next
。
equals
方法逐一比对查找。table[index]
表示经过hash值计算出元素须要存储在数组中的位置(bucket桶),先判断该位置上是否存在Entity。Entity<k,v>
对象,插入结束。若是多线程同时put,若是同时触发了Rehash扩容操做,会致使HashMap中的链表中出现循环节点,进而使得后面get的时候,会出现死循环,因此HashMap是非线程安全的。
先定位到数组元素,再遍历该元素处的链表,在寻找目标元素的时候,除了对比经过key计算出来的hash值,还会用双等或equals方法对key自己来进行比较,二者都为true时才会返回这个元素。
obj1.equals(obj2) = true
,则它们的hashCode必须相同;但若是两个对象不一样,则它们的hashCode不必定不一样。String a = new String(“abc”);
String b = new String(“abc”);
复制代码
若是不覆盖的话,那么a和b的hashCode就会不一样,把这两个类当作key存到HashMap中的话就会出现问题,就会和key的惟一性相矛盾。
如何定位元素?即二进制 hashCode & (leng-1)
- 计算"book"的hashcode 十进制 : 3029737 二进制 : 101110001110101110 1001
- HashMap长度是默认的 16,length - 1 的结果 十进制 : 15 二进制 : 1111
- 把以上两个结果作与运算 101110001110101110 1001 & 1111 = 1001 1001的十进制 : 9,因此 index=9。
JDK1.7中的HashMap是由数组+链表+红黑树组成的,组成链表结点的是Node
包含三个元素:key
、value
和指向下一个Node的next
。
table[i]==null
,直接新建节点添加,转向⑥,若是table[i]不为空,则转向③;hashCode
以及equals
;treeNode
红黑树,若是是红黑树,则直接在树中插入键值对,不然转向⑤;threshold
,若是超过,进行resize()
扩容。public V put(K key, V value) {
// 对key作hash运算获得hashCode
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;
// 步骤①:tab为空则建立。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步骤③:节点key存在,直接覆盖value。
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);
// 判断链表长度是否大于8,若是链表长度大于8转换为红黑树进行处理。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 若是key已经存在而且equals相等,则直接覆盖value。
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;
}
复制代码
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// Node不为空 && 计算索引位置而且索引处有值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断key的hashCode是否相等
// && 判断索引处第一个key与传入key是否相等
// && 判断key的equals是否相等。
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;
}
复制代码
public class HashMapDemo {
@Test
public void test1() {
// 第一种:普通使用,二次取值
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (String key : map.keySet()) {
map.get(key);
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍历:" + (after - before) + "ms");
}
@Test
public void test2() {
// 推荐,尤为是容量大时
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (Map.Entry<String, Object> entry : map.entrySet()) {
entry.getValue();
entry.getKey();
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍历:" + (after - before) + "ms");
}
@Test
public void test3() {
// 经过Map.entrySet使用iterator遍历key和value
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
Iterator<Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = (Entry<String, Object>) iterator.next();
entry.getKey();
entry.getValue();
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍历:" + (after - before) + "ms");
}
@Test
public void test4() {
// 经过Map.values()遍历全部的value,但不能遍历key
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (@SuppressWarnings("unused") Object object : map.values()) {
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍历:" + (after - before) + "ms");
}
}
复制代码