容器--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,这一部分是不太好理解的,个人也没理解得太透彻,所以本文没做介绍,有时间的话再做研究。 

posted @ 2016-08-24 23:52  海上劳工  阅读(267)  评论(0编辑  收藏  举报