Java集合框架之HashMap

我们知道 Java 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。今天我们就来介绍一下最常见的HashMap。

什么是HashMap?

简单的来说,HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。他通过给定的key来取得对应的value值。

如何使用HashMap?

Talk is cheap,show me the code.接下来就进入代码环节。

 

 

  public static void main(String[] args) {
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("222", "bbb");
        hashMap.put("111", "aaa");
        hashMap.put("333", "ccc");
        for (Map.Entry<String, String> entry : hashMap.entrySet()) {
            System.out.println("Key: " + entry.getKey() + " Value: " + entry.getValue());
        }
    }

 



在上述的代码中,我们通过简单的对hashmap存入值并遍历,来简单的体会了它的用法。

那么上述HashMap的key是简单类型的,那么如果key是自定义的类应该怎么办呢?

使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()。为什么呢?这是因为我们如果不重写这两个方法,无论什么情况下两个对象都会被判定为不同,我们也就无法进行我们需要的业务操作。

其实HashMap用途多种多样,我们如果想要深入的理解HashMap就必须深入研究。

HashMap的原理?

JDK1.8之前

JDK1.8之前JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。Hashmap基于数组实现的,通过对key的hashcode & 数组的长度得到在数组中位置,如当前数组有元素,则数组当前元素next指向要插入的元素,这样来解决hash冲突的,形成了拉链式的结构。put时在多线程情况下,会形成环从而导致死循环。数组长度一般是2n,从0开始编号,所以hashcode & (2n-1),(2n-1)每一位都是1,这样会让散列均匀。

JDK1.8之后

HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

 

我们通过HashMap中put方法来解析一下HashMap的源码。

 

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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 ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        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 &&
                ((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;
    }

 

HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

  • 首先会判断桶是否为空,如果为空就执行resize方法。
  • 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。

  •  

    如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e。

  •  

    如果当前桶为红黑树,那就要按照红黑树的方式写入数据。

  •  

    如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。

  •  

    接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。

  •  

    如果在遍历过程中找到 key 相同时直接退出遍历。

  •  

    如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。

  •  

    最后判断是否需要进行扩容。

 

 

以上就是这篇文章的内容,希望对你有所帮助。

 

posted on 2019-07-02 21:48  将图南  阅读(294)  评论(0编辑  收藏  举报