zoukankan      html  css  js  c++  java
  • 容器--HashMap

    一、前言

    看了下上一篇博客已经是半个月前,将近20天前了,很惭愧没有坚持下来,这期间主要是受奥运会和王宝强事件的影响,另外加上HashMap中关于rehash的实现比较不好理解,所以就一拖再拖。如果能坚持,也许容器这一部分已经写完了。

    不管怎么样,奥运总算是结束了,到年底来说,也没有太多可以关注的内容了,坚持下来,做总比不做好。

    回到正题,本文要分析的是Map中的经典实现,HashMap. 顾名思义是通过Hash来查找和存储元素的Map, 这种Map定位方便,支持自动扩容,是一种效率比较高的非线程安全的Map。下面做详细的介绍。

    二、存储介绍

    HashMap的存储相对于之前我们介绍的List要复杂一些,它使用了数组和链表相结合的方式,一个示例结构如下:

    上图大致描述了HashMap的存储结构,即其内部首先是通过对key进行hash, 根据其hash值映射到内部的数组中的某一个位置,但考虑到hash冲突的可能性,所以对于相同hash值而key不同的元素,再通过一个链表进行存储。

    也就是说,实际上这个数组中存储的是链表的头元素,每个节点都是一个Entry类型的对象,其包括了hash值,key, value及指向下一个节点的指针。

    三、实现分析

       根据上一节的介绍,对于map来说,需要实现一些常用而重要的功能,如put,get,delete,search等,本节就来分析一下对于这些功能, hashMap是怎么实现的。

       3.1 添加节点, put.

       根据map的特点,其key是惟一的,而且允许key为null, 所以,在向map中添加一个元素,需要考虑这样几个问题。

       1)key==null的情况下,其hash值如何计算? 

          如果key==null,这种情况下,直接将key映射到下标为0的位置

       2)key已经存在的情况下,如何处理?

          将key的value设置为当前值,原来的值直接返回。

       3)以什么样的机制扩容?

          HashMap内部有一个叫threshold的值,这个值被称之为阈值,初始时,是容量*负载因子,如果当前的map中的元素个数达到这个阈值,且当前映射的位置已经有元素的话,则会对内部的table进行扩容,将其扩大一倍,并对原来的元素重新映射。

          这样做是很有必要的,否则当map中的元素过多的话,就容易造成大量的key映射之后的值是同一个下标,这样就影响了hash的存取效率。

        4)如何查找key是否存在

         如果key为null的话,直接定位到数组中下标为0的位置,否则要先计算hash值,计算完了之后再根据hash值映射到对应的下标。

         找到下标后,要看该下标中如果没有链表,则创建一个,如果有的话,则需要依次遍历链表,比较节点的key是否和当前的key相同,比较的逻辑如下:

         

            int hash = hash(key);
            int i = indexFor(hash, table.length);//table中的位置
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
    
                //entry相同的条件 , hash相同 , key的引用相同,或者equals()
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }

         可以看到,首先要保证hash值一样,再判断key是否相同或equals

         如果没有找到匹配的确实需要添加的话,HashMap采用了个非常巧妙的方式,将元素添加到链表中的头节点,这样省去了遍历。

        3.2 查找

          查找的逻辑其实和添加时对key的查找是类似的,没有找到的话,直接返回null.

      HashMap中对于containsValue的实现比较简单,直接遍历数组的每个下标,再遍历每个下标中的节点,这是一个双重循环,效率相对比较低下,所以这个方法一般情况下不要使用。

        3.3 删除

          删除的查找过程和前面类似,找到后的删除逻辑如下:

    final Entry<K,V> removeEntryForKey(Object key) {
            if (size == 0) {
                return null;
            }
            int hash = (key == null) ? 0 : hash(key);
            int i = indexFor(hash, table.length);
            Entry<K,V> prev = table[i];
            Entry<K,V> e = prev;
    
            while (e != null) {
                Entry<K,V> next = e.next;
                Object k;
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                    modCount++;
                    size--;
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    e.recordRemoval(this);
                    return e;
                }
                prev = e;
                e = next;
            }
    
            return e;
        }

          这里主要是对于链表的修改,如果是头节点则指向下一个,否则指向头节点。

          3.4  遍历

          常见的是获取keySet()以及entrySet(),当然也可以获取值集合values(), 这在内部是通过迭代器来实现的。

          如果我们要遍历整个map, 最理想的方式是获取entrySet(),这样直接取其key和value即可,而不是获取keySet。

    四、总结

         理解了HashMap的存储结构之后,那么对于其各种操作的实现也就不难理解了,hashMap中,如何生成hash值,以及是否需要rehash,这一部分是不太好理解的,个人也没理解得太透彻,所以本文没做介绍,有时间的话再做研究。 

  • 相关阅读:
    python语言程序设计部分习题
    Python基础:Python运行的两种基本方式
    python简介及详细安装方法
    MTBF平均故障间隔时间(转)
    SSH远程登录配置文件sshd_config详解
    SSH服务详解(转)
    GCC编译之后的代码信息
    移动设备识别ID
    STM32CubeMX自建MDK工程的基本步骤
    职位英文缩写
  • 原文地址:https://www.cnblogs.com/macs524/p/5805112.html
Copyright © 2011-2022 走看看