思维导图
代码练习
package hashmaptest;
import linkedlist.Person;
import java.util.HashMap;
import java.util.Map;
public class HashMapTest {
public static void main(String[] args) {
// noParamConstractor();
// oneParamConstractor();
// twoParamConstractor();
// mapParamConstractor();
// putTest();
// removeTest();
// getTest();
// containsKeyTest();
// containsValueTest();
// clearTest();
// sizeTest();
// isEmptyTest();
// cloneTest();
// keySetTest();
// valuesTest();
entrySetTest();
}
private static void entrySetTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
/**
* 遍历HashMap集合的KV时,推荐使用entrySet(),而不是keySet()
*/
hm.entrySet().forEach(entry-> System.out.println(entry.getKey()+":"+entry.getValue()));
System.out.println("====================================");
for (String s : hm.keySet()) {//keySet遍历KV,先要获取keySey迭代器对象,进行迭代,然后根据迭代出来的key再去hm遍历查询value,遍历了两次
System.out.println(s+":"+hm.get(s));
// hm.put("qfc","123");//java.util.ConcurrentModificationException
// hm.remove("重地");//java.util.ConcurrentModificationException
}
}
private static void valuesTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
hm.values().forEach(str-> System.out.println(str));
for (String value : hm.values()) {
// hm.put("qfc","123");//java.util.ConcurrentModificationException
// hm.remove("重地");//java.util.ConcurrentModificationException
}
}
private static void keySetTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
hm.keySet().forEach(str-> System.out.println(str));
for (String s : hm.keySet()) {
// hm.put("qfc","1");//java.util.ConcurrentModificationException
// hm.remove("通话");//java.util.ConcurrentModificationException
}
}
private static void cloneTest() {
HashMap<Person, String> hm = new HashMap<>(8);
Person p1 = new Person("qfc",18);
Person p2 = new Person("zyx",19);
hm.put(p1,"1");
hm.put(p2,"2");
/**
* clone方法
* 首先HashMap clone = super.clone();获得克隆对象
* 然后clone对象重置HashMap的属性,table,threshold,size,modcount,entrySet,keySey,values
* 然后重新将super的key-value重新put进clone中
* 由于clone和super中的对应key和value都是指向同一个对象,所以还是属于浅克隆
*/
HashMap<Person, String> clone = (HashMap) hm.clone();
clone.put(p1,"3");
System.out.println(hm.get(p1));//1
System.out.println(clone.get(p1));//3
Person next = clone.keySet().iterator().next();
next.setAge(100);
Person next1 = hm.keySet().iterator().next();
System.out.println(next1.getAge());//100
for (Map.Entry<Person, String> personStringEntry : hm.entrySet()) {
System.out.println(personStringEntry.getKey());
}
// Person{name='qfc', age=100}
// Person{name='zyx', age=19}
}
private static void isEmptyTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
/**
* isEmpty方法内部即 return size == 0;
* 当判断一个HashMap集合为空时,建议使用isEmpty()而不是size()==0
* 因为isEmpty的时间复杂度是O(1)
*/
System.out.println(hm.isEmpty());
}
private static void sizeTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
/**
* size()方法就是返回HashMap实例的size属性值
*/
int size = hm.size();
System.out.println(size);
}
private static void clearTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
/**
* clear方法是循环遍历哈希表,将哈希表的桶位置全部设置为null
*/
hm.clear();
}
private static void containsValueTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
/**
* containsValue方法是以双重for循环,外层遍历哈希表,内层遍历每个桶下面的链表或红黑树
* 内层以next方式遍历链表或红黑树的节点,红黑树节点TreeNode类是Node类的间接子类,可以支持next方式遍历
* 但是如果是红黑树next效率很低
*/
System.out.println(hm.containsValue("2"));
System.out.println(hm.containsValue("null"));
}
private static void containsKeyTest() {
HashMap<String, String> hm = new HashMap<>(8);
hm.put("重地","1");
hm.put("通话","2");
/**
* containsKey内部调用了getNode方法,即get方法的内部调用逻辑
* 即getNode返回值不为null,则说明有对应key
*/
System.out.println(hm.containsKey("通话"));
System.out.println(hm.containsKey(null));
}
private static void getTest() {
HashMap<String, String> hm = new HashMap<>(8);
/**
* get方法执行前判断底层哈希表是否初始化,且不为空,若没有初始化或者为空,则直接返回null
*/
String r1 = hm.get(null);
System.out.println(r1);//null
hm.put("重地","1");
hm.put("通话","2");
/**
* 若底层哈希表已经初始化,且不为空,则继续检查
* 要查询的key的索引处是否有节点?若没有,则直接返回null
* 否则根据(hash(key)==hash(getkey) && (key==getkey || (key!=null && key.equals(getkey))))
* 比较key和getkey是否相同,若相同,则返回getkey对应键值对的value
* 否则判断桶位置下面是否有节点,若无节点,则返回null
* 若有节点,则遍历红黑树或者链表节点,根据(hash(key)==hash(getkey) && (key==getkey || (key!=null && key.equals(getkey))))
* 判断是否有相同key的节点,若没有则返回null
* 否则返回对应节点的value值
*/
String r2 = hm.get("通话");
System.out.println(r2);//2
}
private static void removeTest() {
HashMap<String, String> hm = new HashMap<>(8);
/**
* remove方法
* 先判断底层哈希表table是否初始化,是否为空?若为空,则remove不只想删除操作,只返回null
* put方法返回值类型是V
*/
String r1 = hm.remove(null);
System.out.println(r1);//null
hm.put("重地","1");
hm.put("通话","2");
/**
* 若底层哈希表已经初始化,且不为空,则判断要删除的key对应的桶位置是否有节点
* 若没有,则不执行删除操作,只返回null
*/
String r2 = hm.remove(null);
System.out.println(r2);//null
/**
* 判断要删除的key对应的桶位置有节点,
* 则根据(hash(key)==hash(delKey) && (key==delkey || (key!=null && key.equals(delkey))))
* 判断key和delkey是否相同。若相同,则直接删除桶位置节点,(下一个节点next上移)
* 若不同,则判断桶位置是否还有下一个节点,若没有,则返回null
* 否则遍历链表节点或者红黑树节点,检查(hash(key)==hash(delKey) && (key==delkey || (key!=null && key.equals(delkey))))
* 是否有相同的节点,若没有,则返回null,否则删除对应相同节点,并返回删除节点的value
*/
String r3 = hm.remove("通话");
System.out.println(r3);//2
}
static void putTest(){
HashMap<String, String> hm = new HashMap<>(8);
//HashMap的key和value都支持存储null,
// 1.key,value的类型是K,V必须是引用类型,引用类型支持null,
// 2.HashMap的底层支持求null的关键码值,即hash(Object key),当key==null,hash(key)=0,即null key总是存储再哈希表的索引0处
hm.put(null,null);
// put方法不仅支持新增,还支持修改。当新增时,说明key对应的索引还没有节点,可以直接赋值,此时返回null,put方法返回值类型是V的类型
String res1 = hm.put("重地", "qfc");
System.out.println(res1);//null
// 当key对应的索引已经有节点时,则此时检查 (hash(key)==hash(oldkey) && (key==oldkey || (key!=null && key.equals(oldKey))))
// 当前key和oldkey通过哈希函数计算出来的索引相同,那么当key和oldkey相同时,
// 则说明此时是put修改功能,put方法会用value覆盖oldValue,put方法返回被覆盖的value
// 否则就是哈希冲突,put则还是新增功能,返回null
String res2 = hm.put("通话", "zyx");
System.out.println(res2);//null
String res3 = hm.put("重地", "qfc1");
System.out.println(res3);//qfc
}
static void noParamConstractor(){
//使用无参构造器创建对象时,底层的哈希表table并没有初始化
// 但是此时已经设置了哈希表的关键属性:threshold=0,loadFactor=0.75
// 注意此时threshold=0表示,初始化table时,table的初始化容量使用默认初始化容量16
HashMap<Integer, String> hm = new HashMap<>();
hm.put(1,"qfc");//第一次put时,内部调用resize方法初始化table为长度16的数组。且更新threshold值为16*0.75=12
hm.put(2,"zyx");
hm.put(3,"qdh");
}
static void oneParamConstractor(){
// 指定初始容量的构造器,使用该构造器创建对象,底层哈希表table同样没有初始化
// 但此时已经设置了哈希表的关键属性:threshold=tabsizeFor(3),loadFactor=0.75
// 其中tableSizeFor的方法作用是将3转为大于或等于它的2的幂,即4。
HashMap<Integer, String> hm = new HashMap<>(3);
hm.put(1,"qfc");//第一次put,内部调用resize方法初始化table为长度4的数组。且更新threshold值为4*0.75=3。
hm.put(2,"zyx");
hm.put(3,"qdh");
hm.put(4,"qdz");// 当添加该键值对时,底层哈希表会再次扩容一次,因为此时size>threshold
/**
* 扩容方案是:容量扩容两倍,容量变为8,threshold也扩容两倍,变为6(只是采用的是capacity*loadFatcor,此时locaFator不变,capacityy扩大2倍)
* 当老数组容量大于等于16时,threshold扩容会采用左移一位方式来提升效率,因为位运算比乘2运算快
*/
}
static void twoParamConstractor(){
/**
* 指定初始化容量和负载因子的构造器,使用该构造器创建对象不会初始化哈希表table
* 但是此时已经设置了哈希表的两个关键属性:threshold:tableSizeFor(3),loadFactor:0.8
*/
HashMap<Integer, String> hm = new HashMap<Integer, String>(3, 0.8f);
hm.put(1,"qfc");//第一次put,内部调用resize方法初始化table为长度4的数组。且更新threshold值为4*0.8=3。
hm.put(2,"zyx");
hm.put(3,"qdh");
hm.put(4,"qdz");// 当添加该键值对时,底层哈希表会再次扩容一次,因为此时size>threshold
/**
* 扩容方案是:容量扩容两倍,容量变为8,threshold也扩容两倍,变为6(只是采用的是capacity*loadFatcor,此时locaFator不变,capacityy扩大2倍)
* 当老数组容量大于等于16时,threshold扩容会采用左移一位方式来提升效率,因为位运算比乘2运算快
*/
}
static void mapParamConstractor(){
HashMap<Integer, String> hm = new HashMap<Integer, String>(3, 0.8f);
hm.put(1,"qfc");
hm.put(2,"zyx");
hm.put(3,"qdh");
hm.put(4,"qdz");
/**
* 指定初始键值对的构造器。使用该构造器创建对象时,已经初始化好了table
* 该构造器使用默认负载因子0.75
*/
HashMap<Integer, String> hm1 = new HashMap<>(hm);
/**
* 该构造器内部会调用putMapEntries方法
* 关于putMapEntries实现
* 首先判断被添加的Map集合hm是否初始化或为空?若hm为空,则hm1的threshold不做处理,还是初始值0
* 若hm不为空,则hm1必须支持存储hm.size()个键值对。但是不能直接将hm.size()设置为hm的哈希表容量
* 因为这样会导致一次扩容。比如hm.size()=4,则存储到3个时,就会触发扩容
*
* 则为了防止扩容操作
* hm1.size()*loadFactor > hm.size()
* 即 hm1.size() > hm.size()/loadFactor
* 那么就将hm1.size() = hm.size()/loadFatcor + 1
* 而由于此时hm1是初始化,所以tableSizeFor(hm1.size()) = threshold
* 则threshold = 8;
*/
}
}
思考题
当你需要存13个键值对到HashMap中,那么使用哪种构造器创建对象比较好?
如果使用无参构造器,则底层哈希表容量为默认初始化容量16,此时扩容阈值是12,则存储第13个元素后,会触发扩容。
如果使用指定容量构造器,那么直接指定初始化容量为13,底层会将tableSizeFor(13)容量,即16容量,作为哈希表容量,同样存储第13个元素后,会触发扩容。
如果使用指定容量构造器,但是预判扩容时机,则指定容量为 tableSizeFor(13 / 0.75 + 1)=32,这样就不会触发扩容。
为什么HashMap的key为自定义对象时,那么该对象的类需要同时重写hashCode和equals方法?
HashMap内部会根据key的关键码值计算出其再哈希表中的索引位置。
key的关键码值计算需要调用(h = key.hashCode) ^ (h>>>16),其中会用到hashCode()方法,所以key需要重写hashCode()方法
当key关键码值计算出来的索引已经有节点占用了,那么就需要判断是否发生哈希冲突?如果key和占用key不相同,则说明发生哈希冲突。
其中判断key和占用oldKey相同的逻辑是:
hash(key) == hash(oldKey) && (key==oldKey || (key!=null && key.equals(oldKey)))
其中用到了key的equals方法。
另外需要保证当hashCode返回值相同时,对应的equals结果也要相同,否则就会很麻烦。
为什么keySet(),values(),entrySet()方法返回的集合在遍历过程中不能新增或删除元素?
因为keySet()返回的时KeySet类对象,该类没有存储数据,而是在遍历时,内部调用迭代器遍历HashMap的哈希表。我们知道迭代器迭代过程中,只能使用使用迭代器进行新增删除,不能使用集合进行新增删除,或者会抛出并发修改异常。
values(),entrySet()也是这个原因
为什么不建议使用keySet()方法去遍历HashMap实例的key-value,而是使用enterySet()方法?
因为keySet()返回的是key列的Set集合。此时keySet()是内部依赖迭代器遍历HashMap的哈希表的所有节点的key。
后面还要hm.get(key)。此时还是需要去基于key去遍历HashMap的哈希表的所有节点的value。
所以使用keySet()遍历KV时,内部遍历了两次HashMap的哈希表
但是enterySet()返回的时key-value键值对Map.Entry类型对象,只需要遍历一次HashMap的哈希表的所有节点,就可以得到所有KV。