Map集合之HashMap(二)

思维导图

代码练习

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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员阿甘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值