HashMap小结:
(1)Map接口的常用实现类:HashMap、Hashtable和Properties
(2)HashMap是Map接口使用频率最高的实现类
(3)HashMap是以key-val对的方式来存储数据(HashMap$Node类型)
(4)key不能重复,但是值可以重复,允许使用null键和null值
(5)如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
我们通过代码来对源码进行解读:
package com.rgf.map; import java.util.HashMap; import java.util.Map; @SuppressWarnings({"all"}) public class Map_ { public static void main(String[] args) { Map map = new HashMap(); map.put("no1","rgf");//k-v map.put("no2","张无忌"); map.put("no1","张三丰");//当有相同的key值时,会替换掉上面的value map.put("no3","张三丰"); map.put(null,null); map.put(null,"abc");//等价替换 map.put("no4",null);//k-v map.put("not5",null);//k-v map.put(1,"赵敏");//k-v map.put(new Object(),"金毛狮王");//k-v //通过get方法,传入key,会返回对应的value System.out.println(map.get(1)); System.out.println("map="+map); } }
我们来把断点放在map.put("no1","张三丰“);
我们下来进行查看:
此时我们的table表为:
我们继续往进走:
我们发现我们进去的hash值与此前的no1的值相同:
我们进入的源码如下所示:
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语句。 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //这里算出来的i的位置为原来存储rgf所在的索引值,为1.。为p不为空。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //这条if语句比较的是key,而不是value。这条语句成立,我们将p即为no1赋值给e. 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; }
hash值相同,导致i相同:
因为不为null,该条语句不符合,继续执行:
该条语句符合,我们将该值赋值给e。
符合之后,我们执行如下语句:
我们将新值赋给之前的value,即e指向的之前的rgf的结点,从而完成替换。
我们发现此时替换成功:
(6)与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(jdk8的hashMap底层:数组+链表+红黑树)
(7)HashMap没有实现同步,因此是线程不安全的。方法没有做同步互斥的操作,没有synchronized
HashMap底层机制及源码剖析:
(1)HashMap底层维护了Node类型的数组table,默认为null。里面的数组为table数组 ,里面的每一个结点为HashMap$Node1结点,而这个实现了Map接口里面的Entry接口。
(2)当创建对象时,将加载因子(loadfactor)初始化为0.75
(3)当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加,如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
(4)第一次添加,则需要扩容table容量为16,临界值(threshold)为12。(16*0.75=12)
(5)以后再扩容,则需要扩容table容量为原来的2倍,即32,临界值为原来的2倍,即24,依此类推
(6)再Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)。
我们来进行源码分析:
我们设计如下代码进行源码分析:
package com.rgf.map;
import java.util.HashMap;
@SuppressWarnings({"all"})
public class HashMapSources1 {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java",10);
map.put("php",10);
map.put("java",20);//替换value
System.out.println("map="+map);
}
}
运行界面如下所示:
我们从HashMap map =new HashMap( )这条开始进行Debug.
1.我们发现先初始化了加载因子:
DEFAULT_LOAD_FACTOR,加载因子为0.75.
完成之后,我们发现此时的table=null.
2.然后我们进入put方法:
我们先进行自由装箱:
退出去之后,我们进入put方法:
里面的hash(key)我们可以通过此算法来计算出hash值。
3.之后我们进入putVal方法:
代码如下所示:,我们再代码进行了详细的见解:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //辅助变量 //如果底层的table数组为null,或者length=0,就扩容到16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //取出hash值对应的table表的索引的位置的Node,如果为null,就直接把加入的k-v创建成一个Node,加入该位置即可。 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 && //如果table表的索引位置的key的hash值和新的key的hash值相同并且满足(现有的结点的key和添加的key是同一个对象 ||equals返回真,就认为不能加入新的k-v. ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)//如果当前存在的table的已有的Node是红黑树,按照红黑树的方式处理 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个后,就调用treeifyBin进行红黑树的转换。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && //如果再循环比较过程中,发现有相同,就break,就只是替换value ((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;//替换,key对应的value。 afterNodeAccess(e); return oldValue; } } ++modCount;//每增加一个Node,就size++ if (++size > threshold)//刚开始12,后面24,48.。。,如果size大于临界值,就进行扩容。 resize(); afterNodeInsertion(evict); return null; }
我查看树化的代码:
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //如果table表为空,或者table表当前的大小是小于64,就暂时不树化,而是进行数组的扩容。 //否则才会进行真正的树化。 //剪枝:树化后随着结点的减少又会退化成链表。 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
如股票我们判断table为空的话,我们进行resize进行扩容。
我们进入resize扩容代码如下所示:
当我们的oldCap不大于0的时候,我们进入下面的else语句:
里面的DEFAULT_INITIAL_CAPACITY默认的初始化的大小为16,
newCap的初始化大小为16,而新建的table表的类型就是Node。
我们从扩容里面跳出来之后,我们发现新建的表的大小为:
之后我们从这个hash值得到这个索引位置:
如果当前位置为空的话,我们就把这个Node值给放到对应的索引位置。
完成这一步之后,我们继续进行下一步,我们发现已经成功的存储进去了:
已经有一个元素了,不再是全空了。
我们继续进行下一步:
其中的thresho为他的临界值,为12. 到了临界值我们就要进行扩容。通过下面的resize进行扩容。
我们返回之后,我们table表里面已经存在java-10了。
我们继续开始debug下一行代码,如下所示:
我们开始继续进行自由装箱:
我们退出自由装箱后,进入put方法:
我们进入hash(key)计算出hash值。我们进入putVal方法:
我们也去判断现在table是否为空,但是之前已经存入了java-10,不等于空了。我们继续进行下一步:
我们索引完之后不是原先的位置,所以也是为空,与此前存的java索引完之后不再同一个位置,之后把php扔进去新的结点。
我们完成之后,我们发现table表又有了新的结点:
我们继续进行下一步之后,我们发现:
此时的size为2,仍然不大于他的临界值,不进行扩容。
完了之后,我们进行返回。
我们debug第三行代码map,put("java",20);
我们进行如下所示:
我们进行自由装箱 :
我们退出去之后,我们进入put方法:
我们之前就有一个java,所以我们通过算法算出的hash值与此前的相同,我们进入putVal源码进行查看:
我们再此前我们算出的java的hash值为3,现在添加的值的key仍然是java,所以我们计算出来的hash值仍然是3,所以此时算出来的索引值所在位置不等于空,不等于空我们进入如下代码:
我们将代码如下所示:
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,else if,else三种情况,
第一种情况为我们先判断hash值是否相同,或者不是同一个对象但是内容相同,出现这种情况,我们不进行添加,即将e指向p.
第二种情况为如果是一个红黑树,按照红黑树的规则进行添加
第三种情况为我们数组后面是一个链表,需要与链表里面的内容进行一一比对。
我们添加的java与此前的相同,既符合第一种情况,直接跳出:
e不等于空。我们进行了如下所示:
我们此时将值进行了替换。
我们发现此时进行了成功的替换。
HashMap扩容树化扩容:
我们设计的的代码如下所示:
package com.rgf.map;
import java.util.HashMap;
import java.util.Objects;
@SuppressWarnings({"all"})
public class HashMapSources2 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 0; i <= 12; i++) {
hashMap.put(new A(i),"hello");
}
System.out.println("hashmap="+hashMap);//12个k-v
}
}
class A{
private int num;
public A(int num) {
this.num = num;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
A a = (A) o;
return num == a.num;
}
//所有的A对象的hashCode都是100
@Override
public int hashCode() {
return 100;
}
}
运行界面如下所示:
我们在 hashMap.put(new A(i),"hello");进行下断点:
因为我们设置得hashcode值相同,所以会放在table表的数组里面的同一个链表下面。
我们发现此时的i为9,但是此时的数组没有满足64我们先不进行树化,而是先进行扩容,我们查看如下所示:
我们发现此时得结点类型仍然是HashMap$Node,
我们扩容两次之后,我们发现如下所示:
此时的结点类型为HashMap$TreeNode.说明此时已经树化了。
我们查看树化的源码如下所示:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//此时树化完成之后,我们按照树化的进行添加元素
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
我们下来进行表的扩容的代码展示:
我们设计的代码如下所示:
package com.rgf.map;
import java.util.HashMap;
import java.util.Objects;
@SuppressWarnings({"all"})
public class HashMapSources2 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 1; i <= 12; i++) {
hashMap.put(i,"hello");
}
hashMap.put("rgf","ypl");
System.out.println("hashmap="+hashMap);//12个k-v
}
}
class A{
private int num;
public A(int num) {
this.num = num;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
A a = (A) o;
return num == a.num;
}
@Override
public String toString() {
return "\nA{" +
"num=" + num +
'}';
}
}
我们运行界面如下所示:
我们进行debug。如下所示:
我们发现此时的table表已经达到临界值,threshold=12,我们新增之后继续进行查看:
我们发现此时扩容为32,即扩容为两倍。 而且新的临界值变成了24:threshold=24
数组扩容为0-->16(12)-->32(24)-->64(64*0.75=48)-->128(96),依此类推,每次扩容两倍。